nulogy_message_bus_consumer 1.0.0.alpha → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/nulogy_message_bus_consumer.rb +16 -7
- data/lib/nulogy_message_bus_consumer/config.rb +7 -1
- data/lib/nulogy_message_bus_consumer/message.rb +11 -13
- data/lib/nulogy_message_bus_consumer/steps/timed_task.rb +42 -0
- data/lib/nulogy_message_bus_consumer/tasks/log_consumer_lag.rb +45 -0
- data/lib/nulogy_message_bus_consumer/tasks/prune_processed_messages.rb +37 -0
- data/lib/nulogy_message_bus_consumer/{steps → tasks}/supervise_consumer_lag.rb +15 -26
- data/lib/nulogy_message_bus_consumer/version.rb +1 -1
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +3 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
- data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
- data/spec/dummy/app/controllers/application_controller.rb +2 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/javascript/packs/application.js +15 -0
- data/spec/dummy/app/jobs/application_job.rb +7 -0
- data/spec/dummy/app/mailers/application_mailer.rb +4 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +33 -0
- data/spec/dummy/config.ru +5 -0
- data/spec/dummy/config/application.rb +29 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/cable.yml +10 -0
- data/spec/dummy/config/credentials/message-bus-us-east-1.key +1 -0
- data/spec/dummy/config/credentials/message-bus-us-east-1.yml.enc +1 -0
- data/spec/dummy/config/database.yml +27 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +62 -0
- data/spec/dummy/config/environments/production.rb +112 -0
- data/spec/dummy/config/environments/test.rb +49 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/spec/dummy/config/initializers/assets.rb +12 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/content_security_policy.rb +28 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/message_bus_consumer.rb +5 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +33 -0
- data/spec/dummy/config/puma.rb +36 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/spring.rb +6 -0
- data/spec/dummy/config/storage.yml +34 -0
- data/spec/dummy/db/schema.rb +21 -0
- data/spec/dummy/log/development.log +4 -0
- data/spec/dummy/log/production.log +18 -0
- data/spec/dummy/log/test.log +7949 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/spec/dummy/public/apple-touch-icon.png +0 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/tmp/development_secret.txt +1 -0
- data/spec/integration/nulogy_message_bus_consumer/auditor_spec.rb +59 -0
- data/spec/integration/nulogy_message_bus_consumer/kafka_utils_spec.rb +41 -0
- data/spec/integration/nulogy_message_bus_consumer/steps/commit_on_success_spec.rb +131 -0
- data/spec/integration/nulogy_message_bus_consumer/steps/connect_to_message_bus_spec.rb +53 -0
- data/spec/integration/nulogy_message_bus_consumer/tasks/prune_processed_messages_spec.rb +32 -0
- data/spec/integration/nulogy_message_bus_consumer/tasks/supervise_consumer_lag_spec.rb +33 -0
- data/spec/integration/test_topic_spec.rb +39 -0
- data/spec/spec_helper.rb +50 -0
- data/spec/support/kafka.rb +74 -0
- data/spec/support/middleware_tap.rb +12 -0
- data/spec/support/skip.rb +9 -0
- data/spec/support/test_topic.rb +48 -0
- data/spec/unit/nulogy_message_bus_consumer/config_spec.rb +20 -0
- data/spec/unit/nulogy_message_bus_consumer/lag_tracker.rb +35 -0
- data/spec/unit/nulogy_message_bus_consumer/message_spec.rb +84 -0
- data/spec/unit/nulogy_message_bus_consumer/pipeline_spec.rb +49 -0
- data/spec/unit/nulogy_message_bus_consumer/steps/commit_on_success_spec.rb +58 -0
- data/spec/unit/nulogy_message_bus_consumer/steps/deduplicate_messages_spec.rb +56 -0
- data/spec/unit/nulogy_message_bus_consumer/steps/log_messages_spec.rb +70 -0
- data/spec/unit/nulogy_message_bus_consumer/steps/stream_messages_spec.rb +35 -0
- data/spec/unit/nulogy_message_bus_consumer/tasks/calculator_spec.rb +67 -0
- data/spec/unit/nulogy_message_bus_consumer_spec.rb +30 -0
- metadata +167 -13
- data/lib/nulogy_message_bus_consumer/steps/log_consumer_lag.rb +0 -51
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0989adbcf8a099ac67362106e1724097f7266523a290a36d568d2f8d195a085a'
|
4
|
+
data.tar.gz: da702aedd0055c0fb3ee4269999bc72009bc6d0823f68264770caa11c10c445c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 690db182773fca7bb41d230eb2a5a2c866f61eae733d1afd63b3f7b4693a747b0d1a82d169f7c09b745d70dd5e945e0c745eebb138e7c2405d608ccb736572ba
|
7
|
+
data.tar.gz: 2f5bf0079b7932e15cf9996518d17ec9309882f601cef507a8952ebf22f4c9da01ba5ede9d77674bc491c2dbc6a04b6fb00d095e106075c9d2ad81a001421b49
|
@@ -16,12 +16,14 @@ require "nulogy_message_bus_consumer/processed_message"
|
|
16
16
|
require "nulogy_message_bus_consumer/steps/commit_on_success"
|
17
17
|
require "nulogy_message_bus_consumer/steps/connect_to_message_bus"
|
18
18
|
require "nulogy_message_bus_consumer/steps/deduplicate_messages"
|
19
|
-
require "nulogy_message_bus_consumer/steps/log_consumer_lag"
|
20
19
|
require "nulogy_message_bus_consumer/steps/log_messages"
|
21
20
|
require "nulogy_message_bus_consumer/steps/seek_beginning_of_topic"
|
22
21
|
require "nulogy_message_bus_consumer/steps/stream_messages"
|
23
22
|
require "nulogy_message_bus_consumer/steps/stream_messages_until_none_are_left"
|
24
|
-
require "nulogy_message_bus_consumer/steps/
|
23
|
+
require "nulogy_message_bus_consumer/steps/timed_task"
|
24
|
+
require "nulogy_message_bus_consumer/tasks/log_consumer_lag"
|
25
|
+
require "nulogy_message_bus_consumer/tasks/prune_processed_messages"
|
26
|
+
require "nulogy_message_bus_consumer/tasks/supervise_consumer_lag"
|
25
27
|
|
26
28
|
module NulogyMessageBusConsumer
|
27
29
|
module_function
|
@@ -49,11 +51,18 @@ module NulogyMessageBusConsumer
|
|
49
51
|
# Note: that since they are before `StreamMessages`, they will only
|
50
52
|
# be called once, without any messages.
|
51
53
|
Steps::ConnectToMessageBus.new(config, logger),
|
52
|
-
Steps::
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
54
|
+
Steps::TimedTask.new(
|
55
|
+
Tasks::LogConsumerLag.new(logger, config.log_lag_interval_seconds)
|
56
|
+
),
|
57
|
+
Steps::TimedTask.new(
|
58
|
+
Tasks::PruneProcessedMessages.new(logger, config.prune_interval_seconds, config.prune_max_age)
|
59
|
+
),
|
60
|
+
Steps::TimedTask.new(
|
61
|
+
Tasks::SuperviseConsumerLag.new(
|
62
|
+
logger,
|
63
|
+
check_interval_seconds: config.lag_check_interval_seconds,
|
64
|
+
tracker: LagTracker.new(failing_checks: config.lag_checks)
|
65
|
+
)
|
57
66
|
),
|
58
67
|
Steps::StreamMessages.new(logger),
|
59
68
|
# Message processing steps start here.
|
@@ -5,12 +5,18 @@ module NulogyMessageBusConsumer
|
|
5
5
|
:consumer_group_id,
|
6
6
|
:lag_check_interval_seconds,
|
7
7
|
:lag_checks,
|
8
|
+
:log_lag_interval_seconds,
|
9
|
+
:prune_interval_seconds,
|
10
|
+
:prune_max_age,
|
8
11
|
:topic_name
|
9
12
|
|
10
13
|
def initialize(options = {})
|
11
14
|
defaults = {
|
12
15
|
lag_check_interval_seconds: 20,
|
13
|
-
lag_checks: 6
|
16
|
+
lag_checks: 6,
|
17
|
+
log_lag_interval_seconds: 1.minute.to_i,
|
18
|
+
prune_interval_seconds: 1.hour.to_i,
|
19
|
+
prune_max_age: 8.days
|
14
20
|
}
|
15
21
|
|
16
22
|
update(defaults.merge(options))
|
@@ -1,17 +1,16 @@
|
|
1
1
|
module NulogyMessageBusConsumer
|
2
2
|
class Message
|
3
|
-
attr_reader :
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
:type
|
3
|
+
attr_reader :event_data
|
4
|
+
attr_reader :event_data_unparsed
|
5
|
+
attr_reader :id
|
6
|
+
attr_reader :key
|
7
|
+
attr_reader :offset
|
8
|
+
attr_reader :partition
|
9
|
+
attr_reader :subscription_id
|
10
|
+
attr_reader :company_uuid
|
11
|
+
attr_reader :timestamp
|
12
|
+
attr_reader :topic
|
13
|
+
attr_reader :created_at
|
15
14
|
|
16
15
|
def initialize(attrs = {})
|
17
16
|
attrs.each { |key, value| instance_variable_set("@#{key}", value) }
|
@@ -37,7 +36,6 @@ module NulogyMessageBusConsumer
|
|
37
36
|
company_uuid: envelope_data[:company_uuid] || envelope_data[:tenant_id],
|
38
37
|
timestamp: kafka_message.timestamp,
|
39
38
|
topic: kafka_message.topic,
|
40
|
-
type: envelope_data[:type],
|
41
39
|
created_at: envelope_data[:created_at]
|
42
40
|
)
|
43
41
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module NulogyMessageBusConsumer
|
2
|
+
module Steps
|
3
|
+
# A generic class to run a "task" on a timer (in a separate thread!)
|
4
|
+
# This class runs the code, the Task does the work
|
5
|
+
#
|
6
|
+
# A Task must implement the methods called in this class:
|
7
|
+
# - #extract_args(kwargs)
|
8
|
+
# Called with the keyword arguments (kwargs) that is passed to this step.
|
9
|
+
# This is a chance to pull out references to pipeline variables (e.g. kafka_consumer)
|
10
|
+
# - #call
|
11
|
+
# The work the Task should perform
|
12
|
+
# - #interval
|
13
|
+
# The time, in seconds, between invocations of #call
|
14
|
+
class TimedTask
|
15
|
+
def initialize(task)
|
16
|
+
@task = task
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(**kwargs)
|
20
|
+
@task.extract_args(**kwargs)
|
21
|
+
|
22
|
+
# Ensure that the process is terminated if there is a problem getting the consumption lag.
|
23
|
+
# This also ensures that the process will terminate on-boot if it cannot connect to Kafka,
|
24
|
+
# allowing the container to be terminated by ECS.
|
25
|
+
Thread.abort_on_exception = true
|
26
|
+
Thread.new { run }
|
27
|
+
|
28
|
+
yield
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def run
|
34
|
+
loop do
|
35
|
+
@task.call
|
36
|
+
|
37
|
+
sleep @task.interval
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module NulogyMessageBusConsumer
|
2
|
+
module Tasks
|
3
|
+
class LogConsumerLag
|
4
|
+
attr_reader :interval
|
5
|
+
|
6
|
+
def initialize(logger, interval_seconds)
|
7
|
+
@logger = logger
|
8
|
+
@interval = interval_seconds
|
9
|
+
end
|
10
|
+
|
11
|
+
def extract_args(kafka_consumer:, **_)
|
12
|
+
@kafka_consumer = kafka_consumer
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
# Delayed start. If we attempt to read consumer#committed immediately, it may fail.
|
17
|
+
# We suspect this is because the consumer#committed is called before the consumer
|
18
|
+
# has finished connecting. There appears to be a race condition.
|
19
|
+
KafkaUtils.wait_for_assignment(@kafka_consumer)
|
20
|
+
|
21
|
+
lag_per_topic = @kafka_consumer.lag(@kafka_consumer.committed)
|
22
|
+
|
23
|
+
@logger.info(JSON.dump({
|
24
|
+
event: "consumer_lag",
|
25
|
+
topics: Calculator.add_max_lag(lag_per_topic)
|
26
|
+
}))
|
27
|
+
$stdout.flush
|
28
|
+
end
|
29
|
+
|
30
|
+
module Calculator
|
31
|
+
def self.add_max_lag(lag_by_topic)
|
32
|
+
lag_by_topic.each_value do |lag_by_partition|
|
33
|
+
lag_by_partition[:_max] = lag_by_partition.values.max || 0
|
34
|
+
end
|
35
|
+
|
36
|
+
lag_by_topic[:_max] = lag_by_topic
|
37
|
+
.map { |_topic, lag_by_partition| lag_by_partition[:_max] }
|
38
|
+
.max || 0
|
39
|
+
|
40
|
+
lag_by_topic
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module NulogyMessageBusConsumer
|
2
|
+
module Tasks
|
3
|
+
# Deletes any ProcessedMessage rows that are older than max_age
|
4
|
+
#
|
5
|
+
# interval_seconds: how many seconds between invocations
|
6
|
+
# max_age: ActiveSupport::Duration of how old ProcessedMessage are
|
7
|
+
# removed. This calls `#ago` on it.
|
8
|
+
# For example, setting this to `1.week` will delete records more than
|
9
|
+
# `1.week.ago` on each invocation.
|
10
|
+
class PruneProcessedMessages
|
11
|
+
attr_reader :interval
|
12
|
+
|
13
|
+
def initialize(logger, interval_seconds, max_age)
|
14
|
+
@logger = logger
|
15
|
+
@interval = interval_seconds
|
16
|
+
@max_age = max_age
|
17
|
+
end
|
18
|
+
|
19
|
+
def extract_args(**_)
|
20
|
+
end
|
21
|
+
|
22
|
+
def call
|
23
|
+
deleted = prune_stale
|
24
|
+
|
25
|
+
@logger.info("Pruned #{deleted} processed messages")
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def prune_stale
|
31
|
+
ProcessedMessage
|
32
|
+
.where(ProcessedMessage.arel_table[:created_at].lt(@max_age.ago))
|
33
|
+
.delete_all
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module NulogyMessageBusConsumer
|
2
|
-
module
|
2
|
+
module Tasks
|
3
3
|
# Supervises the consumer's lag.
|
4
4
|
#
|
5
5
|
# If a partition's lag is non-zero and does not change for an extended period
|
@@ -19,47 +19,36 @@ module NulogyMessageBusConsumer
|
|
19
19
|
#
|
20
20
|
# Killing the main thread causes ECS to restart the task.
|
21
21
|
class SuperviseConsumerLag
|
22
|
+
attr_reader :interval
|
23
|
+
|
22
24
|
def initialize(logger, tracker: NulogyMessageBusConsumer::LagTracker.new(failing_checks: 6), killable: nil, check_interval_seconds: 20)
|
23
25
|
@logger = logger
|
24
26
|
@tracker = tracker
|
25
27
|
@killable = killable
|
26
|
-
@
|
28
|
+
@interval = check_interval_seconds
|
27
29
|
end
|
28
30
|
|
29
|
-
def
|
31
|
+
def extract_args(kafka_consumer:, **_)
|
30
32
|
@consumer = kafka_consumer
|
31
33
|
@killable ||= Thread.current
|
32
|
-
|
33
|
-
run
|
34
|
-
|
35
|
-
yield
|
36
34
|
end
|
37
35
|
|
38
|
-
|
39
|
-
|
40
|
-
def run
|
41
|
-
Thread.abort_on_exception = true
|
36
|
+
def call
|
37
|
+
NulogyMessageBusConsumer::KafkaUtils.wait_for_assignment(@consumer)
|
42
38
|
|
43
|
-
|
44
|
-
|
39
|
+
@tracker.update(@consumer.lag(@consumer.committed))
|
40
|
+
if @tracker.failing?
|
41
|
+
log_failed_partitions
|
45
42
|
|
46
|
-
|
47
|
-
|
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
|
43
|
+
@killable.kill
|
44
|
+
Thread.current.exit
|
58
45
|
end
|
59
46
|
end
|
60
47
|
|
48
|
+
private
|
49
|
+
|
61
50
|
def log_failed_partitions
|
62
|
-
seconds = @
|
51
|
+
seconds = @interval * @tracker.failing_checks
|
63
52
|
failed = @tracker
|
64
53
|
.failed
|
65
54
|
.map { |topic, partitions| "#{topic}: #{partitions.join(",")}" }
|
data/spec/dummy/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,15 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file. JavaScript code in this file should be added after the last require_* statement.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require rails-ujs
|
14
|
+
//= require activestorage
|
15
|
+
//= require_tree .
|
@@ -0,0 +1,7 @@
|
|
1
|
+
class ApplicationJob < ActiveJob::Base
|
2
|
+
# Automatically retry jobs that encountered a deadlock
|
3
|
+
# retry_on ActiveRecord::Deadlocked
|
4
|
+
|
5
|
+
# Most jobs are safe to ignore if the underlying records are no longer available
|
6
|
+
# discard_on ActiveJob::DeserializationError
|
7
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= yield %>
|