nulogy_message_bus_consumer 0.3.2 → 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/Rakefile +2 -3
- 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 +27 -6
- data/lib/nulogy_message_bus_consumer/clock.rb +13 -0
- data/lib/nulogy_message_bus_consumer/config.rb +18 -4
- data/lib/nulogy_message_bus_consumer/deployment/ecs.rb +23 -0
- data/lib/nulogy_message_bus_consumer/lag_tracker.rb +53 -0
- data/lib/nulogy_message_bus_consumer/message.rb +1 -1
- data/lib/nulogy_message_bus_consumer/null_logger.rb +6 -3
- data/lib/nulogy_message_bus_consumer/pipeline.rb +1 -1
- data/lib/nulogy_message_bus_consumer/steps/commit_on_success.rb +2 -1
- data/lib/nulogy_message_bus_consumer/steps/connect_to_message_bus.rb +23 -9
- data/lib/nulogy_message_bus_consumer/steps/deduplicate_messages.rb +1 -1
- data/lib/nulogy_message_bus_consumer/steps/log_messages.rb +4 -11
- 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/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/tasks/supervise_consumer_lag.rb +65 -0
- data/lib/nulogy_message_bus_consumer/version.rb +1 -1
- data/lib/tasks/engine/message_bus_consumer.rake +7 -8
- 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 +209 -21
- data/lib/nulogy_message_bus_consumer/steps/monitor_replication_lag.rb +0 -51
@@ -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
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module NulogyMessageBusConsumer
|
2
|
+
module Tasks
|
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
|
+
attr_reader :interval
|
23
|
+
|
24
|
+
def initialize(logger, tracker: NulogyMessageBusConsumer::LagTracker.new(failing_checks: 6), killable: nil, check_interval_seconds: 20)
|
25
|
+
@logger = logger
|
26
|
+
@tracker = tracker
|
27
|
+
@killable = killable
|
28
|
+
@interval = check_interval_seconds
|
29
|
+
end
|
30
|
+
|
31
|
+
def extract_args(kafka_consumer:, **_)
|
32
|
+
@consumer = kafka_consumer
|
33
|
+
@killable ||= Thread.current
|
34
|
+
end
|
35
|
+
|
36
|
+
def call
|
37
|
+
NulogyMessageBusConsumer::KafkaUtils.wait_for_assignment(@consumer)
|
38
|
+
|
39
|
+
@tracker.update(@consumer.lag(@consumer.committed))
|
40
|
+
if @tracker.failing?
|
41
|
+
log_failed_partitions
|
42
|
+
|
43
|
+
@killable.kill
|
44
|
+
Thread.current.exit
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def log_failed_partitions
|
51
|
+
seconds = @interval * @tracker.failing_checks
|
52
|
+
failed = @tracker
|
53
|
+
.failed
|
54
|
+
.map { |topic, partitions| "#{topic}: #{partitions.join(",")}" }
|
55
|
+
.join(", ")
|
56
|
+
|
57
|
+
@logger.warn(JSON.dump({
|
58
|
+
event: "message_processing_warning",
|
59
|
+
message: "Assigned partition lag has not changed in #{seconds} seconds: #{failed}"
|
60
|
+
}))
|
61
|
+
$stdout.flush
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
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
3
|
task audit: :environment do
|
4
|
-
|
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
15
|
end
|
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 %>
|
data/spec/dummy/bin/rake
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "fileutils"
|
3
|
+
|
4
|
+
# path to your application root.
|
5
|
+
APP_ROOT = File.expand_path("..", __dir__)
|
6
|
+
|
7
|
+
def system!(*args)
|
8
|
+
system(*args) || abort("\n== Command #{args} failed ==")
|
9
|
+
end
|
10
|
+
|
11
|
+
FileUtils.chdir APP_ROOT do
|
12
|
+
# This script is a way to setup or update your development environment automatically.
|
13
|
+
# This script is idempotent, so that you can run it at anytime and get an expectable outcome.
|
14
|
+
# Add necessary setup steps to this file.
|
15
|
+
|
16
|
+
puts "== Installing dependencies =="
|
17
|
+
system! "gem install bundler --conservative"
|
18
|
+
system("bundle check") || system!("bundle install")
|
19
|
+
|
20
|
+
# puts "\n== Copying sample files =="
|
21
|
+
# unless File.exist?('config/database.yml')
|
22
|
+
# FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
|
23
|
+
# end
|
24
|
+
|
25
|
+
puts "\n== Preparing database =="
|
26
|
+
system! "bin/rails db:prepare"
|
27
|
+
|
28
|
+
puts "\n== Removing old logs and tempfiles =="
|
29
|
+
system! "bin/rails log:clear tmp:clear"
|
30
|
+
|
31
|
+
puts "\n== Restarting application server =="
|
32
|
+
system! "bin/rails restart"
|
33
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative "boot"
|
2
|
+
|
3
|
+
require "rails"
|
4
|
+
# Pick the frameworks you want:
|
5
|
+
require "active_model/railtie"
|
6
|
+
require "active_job/railtie"
|
7
|
+
require "active_record/railtie"
|
8
|
+
require "active_storage/engine"
|
9
|
+
require "action_controller/railtie"
|
10
|
+
require "action_mailer/railtie"
|
11
|
+
require "action_view/railtie"
|
12
|
+
require "action_cable/engine"
|
13
|
+
require "sprockets/railtie"
|
14
|
+
# require "rails/test_unit/railtie"
|
15
|
+
|
16
|
+
Bundler.require(*Rails.groups)
|
17
|
+
require "nulogy_message_bus_consumer"
|
18
|
+
|
19
|
+
module Dummy
|
20
|
+
class Application < Rails::Application
|
21
|
+
# Initialize configuration defaults for originally generated Rails version.
|
22
|
+
config.load_defaults 6.0
|
23
|
+
|
24
|
+
# Settings in config/environments/* take precedence over those specified here.
|
25
|
+
# Application configuration can go into files in config/initializers
|
26
|
+
# -- all .rb files in that directory are automatically loaded after loading
|
27
|
+
# the framework and any gems in your application.
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
dfa19863b2709390893da4c2fb85579a
|
@@ -0,0 +1 @@
|
|
1
|
+
../../../../config/credentials/message-bus-us-east-1.yml.enc
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# Copy this file to 'database.yml' and modify the copy as needed.
|
2
|
+
|
3
|
+
defaults: &defaults
|
4
|
+
adapter: postgresql
|
5
|
+
username: nulogy
|
6
|
+
password: <%= ENV['DATABASE_PASS'] || "Nulogy4Ever" %>
|
7
|
+
host: <%= ENV['DATABASE_HOST'] || "localhost" %>
|
8
|
+
port: 5437
|
9
|
+
min_messages: warning
|
10
|
+
schema_search_path: "\"$user\", public, extensions"
|
11
|
+
|
12
|
+
development: &development
|
13
|
+
<<: *defaults
|
14
|
+
database: <%= ENV['DEVELOPMENT_DATABASE'] || "message_bus_consumer_development" %>
|
15
|
+
|
16
|
+
development_slave:
|
17
|
+
<<: *development
|
18
|
+
|
19
|
+
# Warning: The database defined as 'test' will be erased and
|
20
|
+
# re-generated from your development database when you run 'rake'.
|
21
|
+
# Do not set this db to the same as development or production.
|
22
|
+
test: &test
|
23
|
+
<<: *defaults
|
24
|
+
database: <%= ENV['TEST_DATABASE'] || "message_bus_consumer_test" %>
|
25
|
+
|
26
|
+
test_slave:
|
27
|
+
<<: *test
|