nulogy_message_bus_consumer 0.3.0 → 1.0.0.alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +5 -4
- data/config/credentials/message-bus-us-east-1.key +1 -0
- data/config/credentials/message-bus-us-east-1.yml.enc +1 -0
- data/lib/nulogy_message_bus_consumer.rb +18 -6
- data/lib/nulogy_message_bus_consumer/clock.rb +13 -0
- data/lib/nulogy_message_bus_consumer/config.rb +12 -4
- data/lib/nulogy_message_bus_consumer/deployment/ecs.rb +23 -0
- data/lib/nulogy_message_bus_consumer/handlers/log_unprocessed_messages.rb +2 -1
- data/lib/nulogy_message_bus_consumer/kafka_utils.rb +2 -1
- data/lib/nulogy_message_bus_consumer/lag_tracker.rb +53 -0
- data/lib/nulogy_message_bus_consumer/message.rb +21 -12
- data/lib/nulogy_message_bus_consumer/null_logger.rb +6 -3
- data/lib/nulogy_message_bus_consumer/pipeline.rb +6 -3
- data/lib/nulogy_message_bus_consumer/steps/commit_on_success.rb +1 -0
- data/lib/nulogy_message_bus_consumer/steps/connect_to_message_bus.rb +27 -8
- data/lib/nulogy_message_bus_consumer/steps/deduplicate_messages.rb +1 -1
- data/lib/nulogy_message_bus_consumer/steps/{monitor_replication_lag.rb → log_consumer_lag.rb} +3 -3
- data/lib/nulogy_message_bus_consumer/steps/log_messages.rb +14 -3
- data/lib/nulogy_message_bus_consumer/steps/stream_messages.rb +2 -2
- data/lib/nulogy_message_bus_consumer/steps/stream_messages_until_none_are_left.rb +2 -2
- data/lib/nulogy_message_bus_consumer/steps/supervise_consumer_lag.rb +76 -0
- data/lib/nulogy_message_bus_consumer/version.rb +1 -1
- data/lib/tasks/engine/message_bus_consumer.rake +9 -10
- metadata +100 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d17557a978d8762a83afcb1a8027296854d516cde57540243d48c80cc46c012
|
|
4
|
+
data.tar.gz: 02c45bfab242f35a0143d090a08387f80db0030854a28b161792cb09efded53c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8c7ef5e49a3aff7e09e963592fc79f7435a29d3567764d0cec1e98d52fea31d18f1ee2ea4b3d429c2e550307f0fab539733d700716b89b45c05cfee269e87955
|
|
7
|
+
data.tar.gz: 0be71a225c18e458d440f80eefadfb5b1a841ad57d619af434e12832c48ff31c1ce2614aaf5786330e2282e418838a223f91c82ebae74cedda3801446620fbb2
|
data/Rakefile
CHANGED
|
@@ -18,13 +18,14 @@ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
|
|
|
18
18
|
load "rails/tasks/engine.rake"
|
|
19
19
|
load "rails/tasks/statistics.rake"
|
|
20
20
|
|
|
21
|
+
require "rspec/core"
|
|
22
|
+
require "rspec/core/rake_task"
|
|
21
23
|
RSpec::Core::RakeTask.new(:spec)
|
|
22
|
-
require "
|
|
23
|
-
|
|
24
|
-
task default: [:spec, :rubocop]
|
|
24
|
+
require "standard/rake"
|
|
25
|
+
task default: %i[spec standard]
|
|
25
26
|
|
|
26
27
|
require "rake/release"
|
|
27
28
|
|
|
28
29
|
Rake::Release::Task.load_all do |spec|
|
|
29
|
-
spec.version_tag = "
|
|
30
|
+
spec.version_tag = "nulogy_message_bus_consumer-v#{spec.version}"
|
|
30
31
|
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dfa19863b2709390893da4c2fb85579a
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/WXdqUYePaHqAq3P0iTEsrLiMfRKzp2qYYh7K+q6LUNgi4eHNv0+SoLdI1bUNb9UaGvDPGNT3fCwAdkurk5Iud16ok3b4wD6yZ7UkfqbXqZaKH/dciQ5s63p9Hiuq1rbfcqoZ3KR1SXYAwvy8vNqbdwbAzz2N66B1wE5fNibZZlrWzXJjLReiTcNyxNbCPz6vwEwFF52RntuYlIJo4Nkm8vEk3No+HWrOkM8xptr5qApbd+RowLCLZ4/kcAMDB/XPiGobf0AOFv1NUR/9ChEy20usa8Fqd6HtEn4A25HnAC0uaN0K8ZRXjxhMpnXtfMBItn7yxyJ1ubjRZK5a1xVBRU7L/CVV9ZuIsqAHL1++gH5FBrEe83ZIUhN7AzngMDlOPKGiCLiZLrm18I1AEQrD7tJLyXos15AeAzj--cER8cN0iMLwu8Le+--FL7dhMTgr6xL6SkMnYKmeg==
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
require "active_record/railtie"
|
|
2
|
+
require "active_support/core_ext/time/zones"
|
|
1
3
|
require "rdkafka"
|
|
2
4
|
|
|
3
5
|
require "nulogy_message_bus_consumer/engine"
|
|
4
|
-
|
|
6
|
+
require "nulogy_message_bus_consumer/clock"
|
|
5
7
|
require "nulogy_message_bus_consumer/config"
|
|
8
|
+
require "nulogy_message_bus_consumer/deployment/ecs"
|
|
6
9
|
require "nulogy_message_bus_consumer/handlers/log_unprocessed_messages"
|
|
7
10
|
require "nulogy_message_bus_consumer/kafka_utils"
|
|
11
|
+
require "nulogy_message_bus_consumer/lag_tracker"
|
|
8
12
|
require "nulogy_message_bus_consumer/message"
|
|
9
13
|
require "nulogy_message_bus_consumer/null_logger"
|
|
10
14
|
require "nulogy_message_bus_consumer/pipeline"
|
|
@@ -12,11 +16,12 @@ require "nulogy_message_bus_consumer/processed_message"
|
|
|
12
16
|
require "nulogy_message_bus_consumer/steps/commit_on_success"
|
|
13
17
|
require "nulogy_message_bus_consumer/steps/connect_to_message_bus"
|
|
14
18
|
require "nulogy_message_bus_consumer/steps/deduplicate_messages"
|
|
19
|
+
require "nulogy_message_bus_consumer/steps/log_consumer_lag"
|
|
15
20
|
require "nulogy_message_bus_consumer/steps/log_messages"
|
|
16
|
-
require "nulogy_message_bus_consumer/steps/monitor_replication_lag"
|
|
17
21
|
require "nulogy_message_bus_consumer/steps/seek_beginning_of_topic"
|
|
18
22
|
require "nulogy_message_bus_consumer/steps/stream_messages"
|
|
19
23
|
require "nulogy_message_bus_consumer/steps/stream_messages_until_none_are_left"
|
|
24
|
+
require "nulogy_message_bus_consumer/steps/supervise_consumer_lag"
|
|
20
25
|
|
|
21
26
|
module NulogyMessageBusConsumer
|
|
22
27
|
module_function
|
|
@@ -31,7 +36,7 @@ module NulogyMessageBusConsumer
|
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
def logger
|
|
34
|
-
|
|
39
|
+
@logger ||= NullLogger.new
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
def invoke_pipeline(*steps)
|
|
@@ -40,14 +45,21 @@ module NulogyMessageBusConsumer
|
|
|
40
45
|
|
|
41
46
|
def recommended_consumer_pipeline(config: self.config, logger: self.logger)
|
|
42
47
|
Pipeline.new([
|
|
43
|
-
#
|
|
48
|
+
# System processing/health steps.
|
|
49
|
+
# Note: that since they are before `StreamMessages`, they will only
|
|
50
|
+
# be called once, without any messages.
|
|
44
51
|
Steps::ConnectToMessageBus.new(config, logger),
|
|
45
|
-
Steps::
|
|
52
|
+
Steps::LogConsumerLag.new(logger),
|
|
53
|
+
Steps::SuperviseConsumerLag.new(
|
|
54
|
+
logger,
|
|
55
|
+
check_interval_seconds: config.lag_check_interval_seconds,
|
|
56
|
+
tracker: LagTracker.new(failing_checks: config.lag_checks)
|
|
57
|
+
),
|
|
46
58
|
Steps::StreamMessages.new(logger),
|
|
47
59
|
# Message processing steps start here.
|
|
48
60
|
Steps::LogMessages.new(logger),
|
|
49
61
|
Steps::CommitOnSuccess.new,
|
|
50
|
-
Steps::DeduplicateMessages.new(logger)
|
|
62
|
+
Steps::DeduplicateMessages.new(logger)
|
|
51
63
|
])
|
|
52
64
|
end
|
|
53
65
|
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
module NulogyMessageBusConsumer
|
|
2
2
|
class Config
|
|
3
|
-
attr_accessor :
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
attr_accessor :bootstrap_servers,
|
|
4
|
+
:client_id,
|
|
5
|
+
:consumer_group_id,
|
|
6
|
+
:lag_check_interval_seconds,
|
|
7
|
+
:lag_checks,
|
|
8
|
+
:topic_name
|
|
6
9
|
|
|
7
10
|
def initialize(options = {})
|
|
8
|
-
|
|
11
|
+
defaults = {
|
|
12
|
+
lag_check_interval_seconds: 20,
|
|
13
|
+
lag_checks: 6
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
update(defaults.merge(options))
|
|
9
17
|
end
|
|
10
18
|
|
|
11
19
|
def update(options = {})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module NulogyMessageBusConsumer
|
|
2
|
+
module Deployment
|
|
3
|
+
module ECS
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
# Try to get the TaskID from metadata server:
|
|
7
|
+
# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4.html
|
|
8
|
+
# Otherwise, return nil
|
|
9
|
+
def task_id
|
|
10
|
+
data = `curl --silent "$ECS_CONTAINER_METADATA_URI_V4/task"`
|
|
11
|
+
|
|
12
|
+
return if data.empty?
|
|
13
|
+
|
|
14
|
+
json = JSON.parse(data)
|
|
15
|
+
arn = json["TaskARN"]
|
|
16
|
+
|
|
17
|
+
return unless arn
|
|
18
|
+
|
|
19
|
+
arn.split("/").last
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -7,9 +7,10 @@ module NulogyMessageBusConsumer
|
|
|
7
7
|
|
|
8
8
|
def call(message:, **_)
|
|
9
9
|
return if ProcessedMessage.exists?(id: message.id)
|
|
10
|
+
|
|
10
11
|
@logger.warn(JSON.dump(
|
|
11
12
|
event: "unprocessed_message",
|
|
12
|
-
kafka_message: message.to_h
|
|
13
|
+
kafka_message: message.to_h
|
|
13
14
|
))
|
|
14
15
|
end
|
|
15
16
|
end
|
|
@@ -13,13 +13,14 @@ module NulogyMessageBusConsumer
|
|
|
13
13
|
def wait_for(attempts: 100, interval: 0.1)
|
|
14
14
|
attempts.times do
|
|
15
15
|
break if yield
|
|
16
|
+
|
|
16
17
|
sleep interval
|
|
17
18
|
end
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
def every_message_until_none_are_left(consumer)
|
|
21
22
|
Enumerator.new do |yielder|
|
|
22
|
-
while message = consumer.poll(250)
|
|
23
|
+
while (message = consumer.poll(250))
|
|
23
24
|
yielder.yield(message)
|
|
24
25
|
end
|
|
25
26
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require "set"
|
|
2
|
+
|
|
3
|
+
module NulogyMessageBusConsumer
|
|
4
|
+
# Keeps track of how many times a topic's partition has not changed (non-zero) lag between update calls.
|
|
5
|
+
class LagTracker
|
|
6
|
+
attr_reader :failing_checks
|
|
7
|
+
|
|
8
|
+
def initialize(failing_checks: 3)
|
|
9
|
+
@failing_checks = failing_checks
|
|
10
|
+
@tracked = Hash.new { |h, topic| h[topic] = {} }
|
|
11
|
+
@failed = Hash.new { |h, topic| h[topic] = Set.new }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def update(topic_partitions)
|
|
15
|
+
topic_partitions.each_pair do |topic, partitions|
|
|
16
|
+
partitions.each_pair do |partition, value|
|
|
17
|
+
update_topic_partition(topic, partition, value)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def failing?
|
|
23
|
+
@failed.any?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def failed
|
|
27
|
+
@failed.transform_values { |v| v.to_a.sort }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def update_topic_partition(topic, partition, value)
|
|
33
|
+
current_value, count = @tracked.dig(topic, partition)
|
|
34
|
+
|
|
35
|
+
new_value, new_count =
|
|
36
|
+
if current_value == value && !value.zero?
|
|
37
|
+
[current_value, count + 1]
|
|
38
|
+
else
|
|
39
|
+
[value, 0]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@tracked[topic][partition] = [new_value, new_count]
|
|
43
|
+
|
|
44
|
+
if new_count >= @failing_checks
|
|
45
|
+
@failed[topic] << partition
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def exists?(topic, partition)
|
|
50
|
+
@tracked.dig(topic, partition)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
module NulogyMessageBusConsumer
|
|
2
2
|
class Message
|
|
3
|
+
attr_reader :company_uuid,
|
|
4
|
+
:created_at,
|
|
5
|
+
:event_data,
|
|
6
|
+
:event_data_unparsed,
|
|
7
|
+
:id,
|
|
8
|
+
:key,
|
|
9
|
+
:offset,
|
|
10
|
+
:partition,
|
|
11
|
+
:subscription_id,
|
|
12
|
+
:timestamp,
|
|
13
|
+
:topic,
|
|
14
|
+
:type
|
|
15
|
+
|
|
3
16
|
def initialize(attrs = {})
|
|
4
17
|
attrs.each { |key, value| instance_variable_set("@#{key}", value) }
|
|
5
18
|
end
|
|
6
19
|
|
|
7
|
-
attr_reader :event_data
|
|
8
|
-
attr_reader :event_data_unparsed
|
|
9
|
-
attr_reader :id
|
|
10
|
-
attr_reader :key
|
|
11
|
-
attr_reader :offset
|
|
12
|
-
attr_reader :partition
|
|
13
|
-
attr_reader :subscription_id
|
|
14
|
-
attr_reader :company_uuid
|
|
15
|
-
attr_reader :timestamp
|
|
16
|
-
attr_reader :topic
|
|
17
|
-
|
|
18
20
|
def self.from_kafka(kafka_message)
|
|
19
21
|
envelope_data = JSON.parse(kafka_message.payload, symbolize_names: true)
|
|
20
|
-
event_data =
|
|
22
|
+
event_data =
|
|
23
|
+
begin
|
|
24
|
+
JSON.parse(envelope_data[:event_json], symbolize_names: true)
|
|
25
|
+
rescue
|
|
26
|
+
{}
|
|
27
|
+
end
|
|
21
28
|
|
|
22
29
|
new(
|
|
23
30
|
event_data: event_data,
|
|
@@ -30,6 +37,8 @@ module NulogyMessageBusConsumer
|
|
|
30
37
|
company_uuid: envelope_data[:company_uuid] || envelope_data[:tenant_id],
|
|
31
38
|
timestamp: kafka_message.timestamp,
|
|
32
39
|
topic: kafka_message.topic,
|
|
40
|
+
type: envelope_data[:type],
|
|
41
|
+
created_at: envelope_data[:created_at]
|
|
33
42
|
)
|
|
34
43
|
end
|
|
35
44
|
|
|
@@ -9,7 +9,7 @@ module NulogyMessageBusConsumer
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def insert(step, after:)
|
|
12
|
-
index = @steps.find_index { |
|
|
12
|
+
index = @steps.find_index { |s| s.is_a?(after) }
|
|
13
13
|
@steps.insert(index + 1, step)
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -25,7 +25,7 @@ module NulogyMessageBusConsumer
|
|
|
25
25
|
@steps.reverse.reduce(last_step) do |composed_steps, previous_step|
|
|
26
26
|
lambda do |**args|
|
|
27
27
|
invoke_next = compose_with_merged_args(args, composed_steps)
|
|
28
|
-
|
|
28
|
+
previous_step.call(**args, &invoke_next)
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -33,7 +33,10 @@ module NulogyMessageBusConsumer
|
|
|
33
33
|
def compose_with_merged_args(existing_args, func)
|
|
34
34
|
lambda do |**yielded_args|
|
|
35
35
|
args_to_be_overridden = existing_args.keys & yielded_args.keys
|
|
36
|
-
|
|
36
|
+
if args_to_be_overridden.any?
|
|
37
|
+
raise "Cannot override existing argument(s): #{args_to_be_overridden.join(", ")}"
|
|
38
|
+
end
|
|
39
|
+
|
|
37
40
|
func.call(**existing_args.merge(yielded_args))
|
|
38
41
|
end
|
|
39
42
|
end
|
|
@@ -1,33 +1,52 @@
|
|
|
1
1
|
module NulogyMessageBusConsumer
|
|
2
2
|
module Steps
|
|
3
3
|
class ConnectToMessageBus
|
|
4
|
-
def initialize(config, logger)
|
|
4
|
+
def initialize(config, logger, kafka_consumer: nil)
|
|
5
5
|
@config = config
|
|
6
6
|
@logger = logger
|
|
7
|
+
@kafka_consumer = kafka_consumer
|
|
7
8
|
end
|
|
8
9
|
|
|
9
10
|
def call(**_)
|
|
10
11
|
@logger.info("Connecting to the MessageBus")
|
|
11
|
-
consumer = Rdkafka::Config.new(consumer_config).consumer
|
|
12
12
|
@logger.info("Using consumer group id: #{@config.consumer_group_id}")
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
subscribe
|
|
15
|
+
|
|
16
|
+
trap("TERM") { kafka_consumer.close }
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
wait_for_assignment
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
yield(kafka_consumer: consumer)
|
|
20
|
+
yield(kafka_consumer: kafka_consumer)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
private
|
|
24
24
|
|
|
25
|
+
def kafka_consumer
|
|
26
|
+
@kafka_consumer ||= Rdkafka::Config.new(consumer_config).consumer
|
|
27
|
+
end
|
|
28
|
+
|
|
25
29
|
def consumer_config
|
|
26
|
-
{
|
|
30
|
+
config = {
|
|
27
31
|
"bootstrap.servers": @config.bootstrap_servers,
|
|
28
32
|
"enable.auto.commit": false,
|
|
29
33
|
"group.id": @config.consumer_group_id,
|
|
34
|
+
"enable.auto.offset.store": false
|
|
30
35
|
}
|
|
36
|
+
|
|
37
|
+
config["client.id"] = @config.client_id if @config.client_id
|
|
38
|
+
|
|
39
|
+
config
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def subscribe
|
|
43
|
+
kafka_consumer.subscribe(@config.topic_name)
|
|
44
|
+
@logger.info("Listening for kafka messages on topic #{@config.topic_name}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def wait_for_assignment
|
|
48
|
+
KafkaUtils.wait_for_assignment(kafka_consumer)
|
|
49
|
+
@logger.info("Connected as client: #{kafka_consumer.member_id}")
|
|
31
50
|
end
|
|
32
51
|
end
|
|
33
52
|
end
|
data/lib/nulogy_message_bus_consumer/steps/{monitor_replication_lag.rb → log_consumer_lag.rb}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module NulogyMessageBusConsumer
|
|
2
2
|
module Steps
|
|
3
|
-
class
|
|
3
|
+
class LogConsumerLag
|
|
4
4
|
def initialize(logger)
|
|
5
5
|
@logger = logger
|
|
6
6
|
end
|
|
@@ -22,9 +22,9 @@ module NulogyMessageBusConsumer
|
|
|
22
22
|
|
|
23
23
|
@logger.info(JSON.dump({
|
|
24
24
|
event: "consumer_lag",
|
|
25
|
-
topics: Calculator.add_max_lag(lag_per_topic)
|
|
25
|
+
topics: Calculator.add_max_lag(lag_per_topic)
|
|
26
26
|
}))
|
|
27
|
-
|
|
27
|
+
$stdout.flush
|
|
28
28
|
|
|
29
29
|
sleep 60
|
|
30
30
|
end
|
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
module NulogyMessageBusConsumer
|
|
2
2
|
module Steps
|
|
3
3
|
class LogMessages
|
|
4
|
-
def initialize(logger)
|
|
4
|
+
def initialize(logger, clock: Clock.new)
|
|
5
5
|
@logger = logger
|
|
6
|
+
@clock = clock
|
|
6
7
|
end
|
|
7
8
|
|
|
8
9
|
def call(message:, **_)
|
|
9
10
|
@logger.info(JSON.dump({
|
|
10
11
|
event: "message_received",
|
|
11
12
|
kafka_message_id: message.id,
|
|
12
|
-
message: "Received #{message.id}"
|
|
13
|
+
message: "Received #{message.id}"
|
|
13
14
|
}))
|
|
14
15
|
|
|
15
16
|
result = yield
|
|
16
17
|
|
|
18
|
+
millis = diff_millis(message.created_at, @clock.ms)
|
|
17
19
|
@logger.info(JSON.dump({
|
|
18
20
|
event: "message_processed",
|
|
19
21
|
kafka_message_id: message.id,
|
|
20
|
-
message: "Processed #{message.id}",
|
|
22
|
+
message: "Processed #{message.id} (#{message.topic}##{message.partition}@#{message.offset})",
|
|
21
23
|
result: result,
|
|
24
|
+
time_to_processed: millis
|
|
22
25
|
}))
|
|
23
26
|
|
|
24
27
|
result
|
|
25
28
|
end
|
|
29
|
+
|
|
30
|
+
# Debezium appears to be giving us nanos since epoch
|
|
31
|
+
# https://github.com/debezium/debezium/blob/5a115e902cdc1dc399ec02758dd1039a33e99bc2/debezium-core/src/main/java/io/debezium/jdbc/JdbcValueConverters.java#L237
|
|
32
|
+
def diff_millis(oldest_nanos, newest_millis)
|
|
33
|
+
old_millis = oldest_nanos / 1000
|
|
34
|
+
|
|
35
|
+
newest_millis - old_millis
|
|
36
|
+
end
|
|
26
37
|
end
|
|
27
38
|
end
|
|
28
39
|
end
|
|
@@ -12,11 +12,11 @@ module NulogyMessageBusConsumer
|
|
|
12
12
|
kafka_message: kafka_message
|
|
13
13
|
)
|
|
14
14
|
end
|
|
15
|
-
rescue
|
|
15
|
+
rescue => e
|
|
16
16
|
@logger.error(JSON.dump({
|
|
17
17
|
event: "message_processing_errored",
|
|
18
18
|
class: e.class,
|
|
19
|
-
message: e.message
|
|
19
|
+
message: e.message
|
|
20
20
|
}))
|
|
21
21
|
|
|
22
22
|
raise
|
|
@@ -12,11 +12,11 @@ module NulogyMessageBusConsumer
|
|
|
12
12
|
kafka_message: kafka_message
|
|
13
13
|
)
|
|
14
14
|
end
|
|
15
|
-
rescue
|
|
15
|
+
rescue => e
|
|
16
16
|
@logger.error(JSON.dump({
|
|
17
17
|
event: "message_processing_errored",
|
|
18
18
|
class: e.class,
|
|
19
|
-
message: e.message
|
|
19
|
+
message: e.message
|
|
20
20
|
}))
|
|
21
21
|
|
|
22
22
|
raise
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module NulogyMessageBusConsumer
|
|
2
|
+
module Steps
|
|
3
|
+
# Supervises the consumer's lag.
|
|
4
|
+
#
|
|
5
|
+
# If a partition's lag is non-zero and does not change for an extended period
|
|
6
|
+
# of time, then kill the main thread.
|
|
7
|
+
#
|
|
8
|
+
# That period of time is check_interval_seconds * LagTracker#failing_checks
|
|
9
|
+
# With the defaults, that would be 20 * 6 ~ 120 seconds = 2 minutes.
|
|
10
|
+
#
|
|
11
|
+
# Note that this strategy may not work for a busy integration.
|
|
12
|
+
# Consumer lag monitoring should alert in that case.
|
|
13
|
+
# However, this strategy may help alleviate alerts for low traffic or off-peak
|
|
14
|
+
# environments.
|
|
15
|
+
#
|
|
16
|
+
# We've come across cases where the consumer lag is still being logged,
|
|
17
|
+
# messages are being processed, but the consumer is not consuming messages
|
|
18
|
+
# in particular topics.
|
|
19
|
+
#
|
|
20
|
+
# Killing the main thread causes ECS to restart the task.
|
|
21
|
+
class SuperviseConsumerLag
|
|
22
|
+
def initialize(logger, tracker: NulogyMessageBusConsumer::LagTracker.new(failing_checks: 6), killable: nil, check_interval_seconds: 20)
|
|
23
|
+
@logger = logger
|
|
24
|
+
@tracker = tracker
|
|
25
|
+
@killable = killable
|
|
26
|
+
@check_interval_seconds = check_interval_seconds
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call(kafka_consumer:, **_)
|
|
30
|
+
@consumer = kafka_consumer
|
|
31
|
+
@killable ||= Thread.current
|
|
32
|
+
|
|
33
|
+
run
|
|
34
|
+
|
|
35
|
+
yield
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def run
|
|
41
|
+
Thread.abort_on_exception = true
|
|
42
|
+
|
|
43
|
+
Thread.new do
|
|
44
|
+
NulogyMessageBusConsumer::KafkaUtils.wait_for_assignment(@consumer)
|
|
45
|
+
|
|
46
|
+
loop do
|
|
47
|
+
@tracker.update(@consumer.lag(@consumer.committed))
|
|
48
|
+
|
|
49
|
+
if @tracker.failing?
|
|
50
|
+
log_failed_partitions
|
|
51
|
+
|
|
52
|
+
@killable.kill
|
|
53
|
+
Thread.current.exit
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sleep @check_interval_seconds
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def log_failed_partitions
|
|
62
|
+
seconds = @check_interval_seconds * @tracker.failing_checks
|
|
63
|
+
failed = @tracker
|
|
64
|
+
.failed
|
|
65
|
+
.map { |topic, partitions| "#{topic}: #{partitions.join(",")}" }
|
|
66
|
+
.join(", ")
|
|
67
|
+
|
|
68
|
+
@logger.warn(JSON.dump({
|
|
69
|
+
event: "message_processing_warning",
|
|
70
|
+
message: "Assigned partition lag has not changed in #{seconds} seconds: #{failed}"
|
|
71
|
+
}))
|
|
72
|
+
$stdout.flush
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
namespace :message_bus_consumer do
|
|
2
2
|
desc "Verifies that the messages in the message bus have been processed"
|
|
3
|
-
task :
|
|
4
|
-
|
|
3
|
+
task audit: :environment do
|
|
4
|
+
logger = Rails.logger
|
|
5
|
+
config = NulogyMessageBusConsumer::Config.new(
|
|
6
|
+
consumer_group_id: ENV.fetch("MB_AUDIT_GROUP"),
|
|
7
|
+
bootstrap_servers: ENV.fetch("MB_BOOTSTRAP_SERVERS"),
|
|
8
|
+
topic_name: ENV.fetch("MB_CONSUMER_TOPIC")
|
|
9
|
+
)
|
|
5
10
|
|
|
6
11
|
NulogyMessageBusConsumer
|
|
7
|
-
.consumer_audit_pipeline(config: config)
|
|
12
|
+
.consumer_audit_pipeline(config: config, logger: logger)
|
|
8
13
|
.invoke
|
|
9
14
|
end
|
|
10
|
-
|
|
11
|
-
def build_audit_config
|
|
12
|
-
audit_config = NulogyMessageBusConsumer.config.dup
|
|
13
|
-
audit_config.consumer_group_id = "#{audit_config.consumer_group_id}_consumer_audit"
|
|
14
|
-
NulogyMessageBusConsumer.config
|
|
15
|
-
end
|
|
16
|
-
end
|
|
15
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nulogy_message_bus_consumer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0.alpha
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nulogy
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2021-04-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -52,34 +52,104 @@ dependencies:
|
|
|
52
52
|
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: bundler-audit
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - '='
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: 0.7.0.1
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - '='
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: 0.7.0.1
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: dotenv
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - '='
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: 2.7.6
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - '='
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 2.7.6
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: pg
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - '='
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: 1.2.3
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - '='
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: 1.2.3
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: pry
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: pry-byebug
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0'
|
|
55
125
|
- !ruby/object:Gem::Dependency
|
|
56
126
|
name: rails
|
|
57
127
|
requirement: !ruby/object:Gem::Requirement
|
|
58
128
|
requirements:
|
|
59
129
|
- - '='
|
|
60
130
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 6.0.3
|
|
131
|
+
version: 6.0.3.5
|
|
62
132
|
type: :development
|
|
63
133
|
prerelease: false
|
|
64
134
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
135
|
requirements:
|
|
66
136
|
- - '='
|
|
67
137
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 6.0.3
|
|
138
|
+
version: 6.0.3.5
|
|
69
139
|
- !ruby/object:Gem::Dependency
|
|
70
140
|
name: rake-release
|
|
71
141
|
requirement: !ruby/object:Gem::Requirement
|
|
72
142
|
requirements:
|
|
73
143
|
- - '='
|
|
74
144
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: 1.
|
|
145
|
+
version: 1.3.0
|
|
76
146
|
type: :development
|
|
77
147
|
prerelease: false
|
|
78
148
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
149
|
requirements:
|
|
80
150
|
- - '='
|
|
81
151
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: 1.
|
|
152
|
+
version: 1.3.0
|
|
83
153
|
- !ruby/object:Gem::Dependency
|
|
84
154
|
name: rspec
|
|
85
155
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -95,75 +165,75 @@ dependencies:
|
|
|
95
165
|
- !ruby/object:Gem::Version
|
|
96
166
|
version: 3.9.0
|
|
97
167
|
- !ruby/object:Gem::Dependency
|
|
98
|
-
name: rspec-
|
|
168
|
+
name: rspec-json_expectations
|
|
99
169
|
requirement: !ruby/object:Gem::Requirement
|
|
100
170
|
requirements:
|
|
101
171
|
- - '='
|
|
102
172
|
- !ruby/object:Gem::Version
|
|
103
|
-
version:
|
|
173
|
+
version: 2.2.0
|
|
104
174
|
type: :development
|
|
105
175
|
prerelease: false
|
|
106
176
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
177
|
requirements:
|
|
108
178
|
- - '='
|
|
109
179
|
- !ruby/object:Gem::Version
|
|
110
|
-
version:
|
|
180
|
+
version: 2.2.0
|
|
111
181
|
- !ruby/object:Gem::Dependency
|
|
112
|
-
name: rspec-
|
|
182
|
+
name: rspec-rails
|
|
113
183
|
requirement: !ruby/object:Gem::Requirement
|
|
114
184
|
requirements:
|
|
115
185
|
- - '='
|
|
116
186
|
- !ruby/object:Gem::Version
|
|
117
|
-
version:
|
|
187
|
+
version: 4.0.1
|
|
118
188
|
type: :development
|
|
119
189
|
prerelease: false
|
|
120
190
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
191
|
requirements:
|
|
122
192
|
- - '='
|
|
123
193
|
- !ruby/object:Gem::Version
|
|
124
|
-
version:
|
|
194
|
+
version: 4.0.1
|
|
125
195
|
- !ruby/object:Gem::Dependency
|
|
126
|
-
name:
|
|
196
|
+
name: standard
|
|
127
197
|
requirement: !ruby/object:Gem::Requirement
|
|
128
198
|
requirements:
|
|
129
199
|
- - '='
|
|
130
200
|
- !ruby/object:Gem::Version
|
|
131
|
-
version: 0.
|
|
201
|
+
version: 0.11.0
|
|
132
202
|
type: :development
|
|
133
203
|
prerelease: false
|
|
134
204
|
version_requirements: !ruby/object:Gem::Requirement
|
|
135
205
|
requirements:
|
|
136
206
|
- - '='
|
|
137
207
|
- !ruby/object:Gem::Version
|
|
138
|
-
version: 0.
|
|
208
|
+
version: 0.11.0
|
|
139
209
|
- !ruby/object:Gem::Dependency
|
|
140
|
-
name: rubocop-
|
|
210
|
+
name: rubocop-rails
|
|
141
211
|
requirement: !ruby/object:Gem::Requirement
|
|
142
212
|
requirements:
|
|
143
213
|
- - '='
|
|
144
214
|
- !ruby/object:Gem::Version
|
|
145
|
-
version:
|
|
215
|
+
version: 2.5.2
|
|
146
216
|
type: :development
|
|
147
217
|
prerelease: false
|
|
148
218
|
version_requirements: !ruby/object:Gem::Requirement
|
|
149
219
|
requirements:
|
|
150
220
|
- - '='
|
|
151
221
|
- !ruby/object:Gem::Version
|
|
152
|
-
version:
|
|
222
|
+
version: 2.5.2
|
|
153
223
|
- !ruby/object:Gem::Dependency
|
|
154
|
-
name:
|
|
224
|
+
name: rubocop-rspec
|
|
155
225
|
requirement: !ruby/object:Gem::Requirement
|
|
156
226
|
requirements:
|
|
157
227
|
- - '='
|
|
158
228
|
- !ruby/object:Gem::Version
|
|
159
|
-
version: 1.
|
|
229
|
+
version: 1.38.1
|
|
160
230
|
type: :development
|
|
161
231
|
prerelease: false
|
|
162
232
|
version_requirements: !ruby/object:Gem::Requirement
|
|
163
233
|
requirements:
|
|
164
234
|
- - '='
|
|
165
235
|
- !ruby/object:Gem::Version
|
|
166
|
-
version: 1.
|
|
236
|
+
version: 1.38.1
|
|
167
237
|
description:
|
|
168
238
|
email:
|
|
169
239
|
- tass@nulogy.com
|
|
@@ -172,13 +242,18 @@ extensions: []
|
|
|
172
242
|
extra_rdoc_files: []
|
|
173
243
|
files:
|
|
174
244
|
- Rakefile
|
|
245
|
+
- config/credentials/message-bus-us-east-1.key
|
|
246
|
+
- config/credentials/message-bus-us-east-1.yml.enc
|
|
175
247
|
- config/routes.rb
|
|
176
248
|
- db/migrate/20200509095105_create_message_bus_processed_messages.rb
|
|
177
249
|
- lib/nulogy_message_bus_consumer.rb
|
|
250
|
+
- lib/nulogy_message_bus_consumer/clock.rb
|
|
178
251
|
- lib/nulogy_message_bus_consumer/config.rb
|
|
252
|
+
- lib/nulogy_message_bus_consumer/deployment/ecs.rb
|
|
179
253
|
- lib/nulogy_message_bus_consumer/engine.rb
|
|
180
254
|
- lib/nulogy_message_bus_consumer/handlers/log_unprocessed_messages.rb
|
|
181
255
|
- lib/nulogy_message_bus_consumer/kafka_utils.rb
|
|
256
|
+
- lib/nulogy_message_bus_consumer/lag_tracker.rb
|
|
182
257
|
- lib/nulogy_message_bus_consumer/message.rb
|
|
183
258
|
- lib/nulogy_message_bus_consumer/null_logger.rb
|
|
184
259
|
- lib/nulogy_message_bus_consumer/pipeline.rb
|
|
@@ -186,11 +261,12 @@ files:
|
|
|
186
261
|
- lib/nulogy_message_bus_consumer/steps/commit_on_success.rb
|
|
187
262
|
- lib/nulogy_message_bus_consumer/steps/connect_to_message_bus.rb
|
|
188
263
|
- lib/nulogy_message_bus_consumer/steps/deduplicate_messages.rb
|
|
264
|
+
- lib/nulogy_message_bus_consumer/steps/log_consumer_lag.rb
|
|
189
265
|
- lib/nulogy_message_bus_consumer/steps/log_messages.rb
|
|
190
|
-
- lib/nulogy_message_bus_consumer/steps/monitor_replication_lag.rb
|
|
191
266
|
- lib/nulogy_message_bus_consumer/steps/seek_beginning_of_topic.rb
|
|
192
267
|
- lib/nulogy_message_bus_consumer/steps/stream_messages.rb
|
|
193
268
|
- lib/nulogy_message_bus_consumer/steps/stream_messages_until_none_are_left.rb
|
|
269
|
+
- lib/nulogy_message_bus_consumer/steps/supervise_consumer_lag.rb
|
|
194
270
|
- lib/nulogy_message_bus_consumer/version.rb
|
|
195
271
|
- lib/tasks/engine/message_bus_consumer.rake
|
|
196
272
|
homepage: https://github.com/nulogy/message-bus/tree/master/gems/nulogy_message_bus_consumer
|
|
@@ -208,9 +284,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
208
284
|
version: '0'
|
|
209
285
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
210
286
|
requirements:
|
|
211
|
-
- - "
|
|
287
|
+
- - ">"
|
|
212
288
|
- !ruby/object:Gem::Version
|
|
213
|
-
version:
|
|
289
|
+
version: 1.3.1
|
|
214
290
|
requirements: []
|
|
215
291
|
rubygems_version: 3.0.3
|
|
216
292
|
signing_key:
|