dionysus-rb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +61 -0
  3. data/.github/workflows/ci.yml +77 -0
  4. data/.gitignore +12 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +175 -0
  7. data/.rubocop_todo.yml +53 -0
  8. data/CHANGELOG.md +227 -0
  9. data/Gemfile +10 -0
  10. data/Gemfile.lock +258 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +1206 -0
  13. data/Rakefile +10 -0
  14. data/assets/logo.svg +51 -0
  15. data/bin/console +11 -0
  16. data/bin/karafka_health_check +14 -0
  17. data/bin/outbox_worker_health_check +12 -0
  18. data/bin/setup +8 -0
  19. data/dionysus-rb.gemspec +64 -0
  20. data/docker-compose.yml +44 -0
  21. data/lib/dionysus/checks/health_check.rb +50 -0
  22. data/lib/dionysus/checks.rb +7 -0
  23. data/lib/dionysus/consumer/batch_events_publisher.rb +33 -0
  24. data/lib/dionysus/consumer/config.rb +97 -0
  25. data/lib/dionysus/consumer/deserializer.rb +231 -0
  26. data/lib/dionysus/consumer/dionysus_event.rb +42 -0
  27. data/lib/dionysus/consumer/karafka_consumer_generator.rb +56 -0
  28. data/lib/dionysus/consumer/params_batch_processor.rb +65 -0
  29. data/lib/dionysus/consumer/params_batch_transformations/remove_duplicates_strategy.rb +54 -0
  30. data/lib/dionysus/consumer/params_batch_transformations.rb +4 -0
  31. data/lib/dionysus/consumer/persistor.rb +157 -0
  32. data/lib/dionysus/consumer/registry.rb +84 -0
  33. data/lib/dionysus/consumer/synced_data/assign_columns_from_synced_data.rb +27 -0
  34. data/lib/dionysus/consumer/synced_data/assign_columns_from_synced_data_job.rb +26 -0
  35. data/lib/dionysus/consumer/synced_data.rb +4 -0
  36. data/lib/dionysus/consumer/synchronizable_model.rb +93 -0
  37. data/lib/dionysus/consumer/workers_group.rb +18 -0
  38. data/lib/dionysus/consumer.rb +36 -0
  39. data/lib/dionysus/monitor.rb +48 -0
  40. data/lib/dionysus/producer/base_responder.rb +46 -0
  41. data/lib/dionysus/producer/config.rb +104 -0
  42. data/lib/dionysus/producer/deleted_record_serializer.rb +17 -0
  43. data/lib/dionysus/producer/genesis/performed.rb +11 -0
  44. data/lib/dionysus/producer/genesis/stream_job.rb +13 -0
  45. data/lib/dionysus/producer/genesis/streamer/base_job.rb +44 -0
  46. data/lib/dionysus/producer/genesis/streamer/standard_job.rb +43 -0
  47. data/lib/dionysus/producer/genesis/streamer.rb +40 -0
  48. data/lib/dionysus/producer/genesis.rb +62 -0
  49. data/lib/dionysus/producer/karafka_responder_generator.rb +133 -0
  50. data/lib/dionysus/producer/key.rb +14 -0
  51. data/lib/dionysus/producer/model_serializer.rb +105 -0
  52. data/lib/dionysus/producer/outbox/active_record_publishable.rb +74 -0
  53. data/lib/dionysus/producer/outbox/datadog_latency_reporter.rb +26 -0
  54. data/lib/dionysus/producer/outbox/datadog_latency_reporter_job.rb +11 -0
  55. data/lib/dionysus/producer/outbox/datadog_latency_reporter_scheduler.rb +47 -0
  56. data/lib/dionysus/producer/outbox/datadog_tracer.rb +32 -0
  57. data/lib/dionysus/producer/outbox/duplicates_filter.rb +26 -0
  58. data/lib/dionysus/producer/outbox/event_name.rb +26 -0
  59. data/lib/dionysus/producer/outbox/health_check.rb +48 -0
  60. data/lib/dionysus/producer/outbox/latency_tracker.rb +43 -0
  61. data/lib/dionysus/producer/outbox/model.rb +117 -0
  62. data/lib/dionysus/producer/outbox/producer.rb +26 -0
  63. data/lib/dionysus/producer/outbox/publishable.rb +106 -0
  64. data/lib/dionysus/producer/outbox/publisher.rb +131 -0
  65. data/lib/dionysus/producer/outbox/records_processor.rb +56 -0
  66. data/lib/dionysus/producer/outbox/runner.rb +120 -0
  67. data/lib/dionysus/producer/outbox/tombstone_publisher.rb +22 -0
  68. data/lib/dionysus/producer/outbox.rb +103 -0
  69. data/lib/dionysus/producer/partition_key.rb +42 -0
  70. data/lib/dionysus/producer/registry/validator.rb +32 -0
  71. data/lib/dionysus/producer/registry.rb +165 -0
  72. data/lib/dionysus/producer/serializer.rb +52 -0
  73. data/lib/dionysus/producer/suppressor.rb +18 -0
  74. data/lib/dionysus/producer.rb +121 -0
  75. data/lib/dionysus/railtie.rb +9 -0
  76. data/lib/dionysus/rb/version.rb +5 -0
  77. data/lib/dionysus/rb.rb +8 -0
  78. data/lib/dionysus/support/rspec/outbox_publishable.rb +78 -0
  79. data/lib/dionysus/topic_name.rb +15 -0
  80. data/lib/dionysus/utils/default_message_filter.rb +25 -0
  81. data/lib/dionysus/utils/exponential_backoff.rb +7 -0
  82. data/lib/dionysus/utils/karafka_datadog_listener.rb +20 -0
  83. data/lib/dionysus/utils/karafka_sentry_listener.rb +9 -0
  84. data/lib/dionysus/utils/null_error_handler.rb +6 -0
  85. data/lib/dionysus/utils/null_event_bus.rb +5 -0
  86. data/lib/dionysus/utils/null_hermes_event_producer.rb +5 -0
  87. data/lib/dionysus/utils/null_instrumenter.rb +7 -0
  88. data/lib/dionysus/utils/null_lock_client.rb +13 -0
  89. data/lib/dionysus/utils/null_model_factory.rb +5 -0
  90. data/lib/dionysus/utils/null_mutex_provider.rb +7 -0
  91. data/lib/dionysus/utils/null_retry_provider.rb +7 -0
  92. data/lib/dionysus/utils/null_tracer.rb +5 -0
  93. data/lib/dionysus/utils/null_transaction_provider.rb +15 -0
  94. data/lib/dionysus/utils/sidekiq_batched_job_distributor.rb +24 -0
  95. data/lib/dionysus/utils.rb +6 -0
  96. data/lib/dionysus/version.rb +7 -0
  97. data/lib/dionysus-rb.rb +3 -0
  98. data/lib/dionysus.rb +133 -0
  99. data/lib/tasks/dionysus.rake +18 -0
  100. data/log/development.log +0 -0
  101. data/sig/dionysus/rb.rbs +6 -0
  102. metadata +585 -0
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string"
4
+
5
+ class Dionysus::Producer::KarafkaResponderGenerator
6
+ TOMBSTONE = nil
7
+
8
+ def generate(config, topic)
9
+ topic_name = topic.to_s
10
+ genesis_topic_name = topic.genesis_to_s if topic.genesis_replica?
11
+
12
+ responder_klass = Class.new(Dionysus::Producer::BaseResponder) do
13
+ topic topic_name
14
+ topic genesis_topic_name if topic.genesis_replica?
15
+
16
+ define_method :respond do |batch, options = {}|
17
+ config.instrumenter.instrument("dionysus.respond.#{self.class.name}") do
18
+ final_options = {}
19
+ if (partition_key = options.fetch(:partition_key, nil))
20
+ final_options[:partition_key] = partition_key
21
+ end
22
+ if (key = options.fetch(:key, nil))
23
+ final_options[:key] = key
24
+ end
25
+
26
+ if genesis_only?(options) && genesis_topic_name.nil?
27
+ raise "cannot execute genesis-only as there is no genesis topic for responder #{self.class.name}"
28
+ end
29
+
30
+ if batch.nil?
31
+ unless genesis_only?(options)
32
+ respond_to topic_name, TOMBSTONE, **final_options
33
+ config.event_bus.publish("dionysus.respond", topic_name: topic_name, message: TOMBSTONE,
34
+ options: final_options)
35
+ end
36
+ if topic.genesis_replica?
37
+ respond_to genesis_topic_name, TOMBSTONE, **final_options
38
+ config.event_bus.publish("dionysus.respond", topic_name: genesis_topic_name, message: TOMBSTONE,
39
+ options: final_options)
40
+ end
41
+ else
42
+ message = Array.wrap(batch).map do |event, record_or_records, batch_options|
43
+ records = Array.wrap(record_or_records)
44
+ return if records.empty?
45
+
46
+ record = records.sample
47
+
48
+ payload = serialize_to_payload(records, topic, batch_options)
49
+
50
+ {
51
+ event: event,
52
+ model_name: record.model_name.name,
53
+ data: payload
54
+ }
55
+ end
56
+ unless genesis_only?(options)
57
+ respond_to topic_name, { message: message }, **final_options
58
+ config.event_bus.publish("dionysus.respond", topic_name: topic_name, message: message,
59
+ options: final_options)
60
+ end
61
+ if topic.genesis_replica?
62
+ respond_to genesis_topic_name, { message: message }, **final_options
63
+ config.event_bus.publish("dionysus.respond", topic_name: genesis_topic_name, message: message,
64
+ options: final_options)
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ define_method :serialize_to_payload do |records, current_topic, batch_options|
73
+ if batch_options.to_h[:serialize] == false
74
+ records.map(&:as_json)
75
+ else
76
+ record = records.sample
77
+
78
+ model_klass = record.class
79
+ dependencies = current_topic
80
+ .models
81
+ .find(-> { NullRegistration.new }) { |model_registration| model_registration.model_klass == model_klass }
82
+ .options
83
+ .to_h
84
+ .fetch(:with, [])
85
+
86
+ current_topic.serializer_klass.serialize(records, dependencies: dependencies)
87
+ end
88
+ end
89
+
90
+ define_method :genesis_only? do |options|
91
+ options.fetch(:genesis_only, false) == true
92
+ end
93
+ end
94
+
95
+ responder_klass.instance_exec(topic) do |dionysus_topic|
96
+ define_singleton_method :publisher_of? do |model_klass|
97
+ dionysus_topic.publishes_model?(model_klass)
98
+ end
99
+
100
+ define_singleton_method :publisher_for_topic? do |current_topic|
101
+ if dionysus_topic.genesis_replica?
102
+ dionysus_topic.to_s == current_topic.to_s || dionysus_topic.genesis_to_s == current_topic.to_s
103
+ else
104
+ dionysus_topic.to_s == current_topic.to_s
105
+ end
106
+ end
107
+
108
+ define_singleton_method :publisher_of_model_for_topic? do |model_klass, current_topic|
109
+ dionysus_topic.publishes_model?(model_klass) && publisher_for_topic?(current_topic)
110
+ end
111
+
112
+ define_singleton_method :partition_key do
113
+ dionysus_topic.partition_key
114
+ end
115
+
116
+ define_singleton_method :primary_topic do
117
+ responder_klass.topics.values.first
118
+ end
119
+ end
120
+
121
+ responder_klass_name = "#{topic.to_s.classify}Responder"
122
+
123
+ Dionysus.send(:remove_const, responder_klass_name) if Dionysus.const_defined?(responder_klass_name)
124
+ Dionysus.const_set(responder_klass_name, responder_klass)
125
+ responder_klass
126
+ end
127
+
128
+ class NullRegistration
129
+ def options
130
+ {}
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Producer::Key
4
+ attr_reader :resource
5
+ private :resource
6
+
7
+ def initialize(resource)
8
+ @resource = resource
9
+ end
10
+
11
+ def to_key
12
+ "#{resource.model_name}:#{resource.id}"
13
+ end
14
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Producer::ModelSerializer
4
+ attr_reader :record, :include, :context_serializer
5
+ private :record, :include, :context_serializer
6
+
7
+ def initialize(record, include:, context_serializer:)
8
+ @record = record
9
+ @include = include
10
+ @context_serializer = context_serializer
11
+ end
12
+
13
+ def self.attributes(*names)
14
+ Array(names).each do |name|
15
+ attribute(name, {})
16
+ end
17
+ end
18
+
19
+ def self.attribute(name, options = {})
20
+ declared_attributes << [name, options]
21
+
22
+ define_method(name) do
23
+ record.public_send(name)
24
+ end
25
+ end
26
+
27
+ def self.has_one(name, options = {})
28
+ declared_to_one_relationships << [name, options]
29
+
30
+ define_method(name) do
31
+ record.public_send(name)
32
+ end
33
+
34
+ define_method("#{name}_id") do
35
+ record.public_send("#{name}_id")
36
+ end
37
+ end
38
+
39
+ def self.has_many(name, options = {})
40
+ declared_to_many_relationships << [name, options]
41
+
42
+ define_method(name) do
43
+ record.public_send(name)
44
+ end
45
+
46
+ define_method("#{name.to_s.singularize}_ids") do
47
+ record.public_send("#{name.to_s.singularize}_ids")
48
+ end
49
+ end
50
+
51
+ def self.declared_attributes
52
+ @declared_attributes ||= []
53
+ end
54
+
55
+ def self.declared_to_one_relationships
56
+ @declared_to_one_relationships ||= []
57
+ end
58
+
59
+ def self.declared_to_many_relationships
60
+ @declared_to_many_relationships ||= []
61
+ end
62
+
63
+ def as_json
64
+ {}.tap do |payload|
65
+ declared_attributes.each do |declared_attribute, _options|
66
+ payload[declared_attribute] = send(declared_attribute)
67
+ end
68
+ payload["links"] = {}
69
+ declared_to_one_relationships.each do |declared_relationship, _options|
70
+ payload["links"][declared_relationship] = send("#{declared_relationship}_id")
71
+ end
72
+ declared_to_many_relationships.each do |declared_relationship, _options|
73
+ payload["links"][declared_relationship] = send("#{declared_relationship.to_s.singularize}_ids")
74
+ end
75
+
76
+ include.each do |relationship_to_include|
77
+ relationship_to_include = relationship_to_include.to_sym
78
+
79
+ if declared_to_one_relationships.to_h.key?(relationship_to_include)
80
+ payload[relationship_to_include] =
81
+ context_serializer.serialize(send(relationship_to_include), dependencies: []).first
82
+ end
83
+
84
+ if declared_to_many_relationships.to_h.key?(relationship_to_include)
85
+ payload[relationship_to_include] =
86
+ context_serializer.serialize(send(relationship_to_include), dependencies: [])
87
+ end
88
+ end
89
+ end.deep_stringify_keys
90
+ end
91
+
92
+ private
93
+
94
+ def declared_attributes
95
+ self.class.declared_attributes
96
+ end
97
+
98
+ def declared_to_one_relationships
99
+ self.class.declared_to_one_relationships
100
+ end
101
+
102
+ def declared_to_many_relationships
103
+ self.class.declared_to_many_relationships
104
+ end
105
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Dionysus::Producer::Outbox::ActiveRecordPublishable
6
+ extend ActiveSupport::Concern
7
+
8
+ OUTBOX_RECORDS_TO_PUBLISH_STORAGE_KEY = :bookingsync_outbox_records_to_publish
9
+ private_constant :OUTBOX_RECORDS_TO_PUBLISH_STORAGE_KEY
10
+
11
+ included do
12
+ after_create :dionysus_insert_model_created
13
+ after_update :dionysus_insert_model_updated
14
+ after_destroy :dionysus_insert_model_destroy
15
+ after_commit :publish_outbox_records
16
+
17
+ def self.outbox_records_to_publish
18
+ Thread.current[OUTBOX_RECORDS_TO_PUBLISH_STORAGE_KEY] ||= Concurrent::Array.new
19
+ end
20
+
21
+ def self.add_outbox_records_to_publish(records)
22
+ outbox_records_to_publish.concat(records)
23
+ end
24
+
25
+ def self.clear_records_to_publish
26
+ Thread.current[OUTBOX_RECORDS_TO_PUBLISH_STORAGE_KEY] = Concurrent::Array.new
27
+ end
28
+ end
29
+
30
+ def dionysus_publish_updates_after_soft_delete?
31
+ false
32
+ end
33
+
34
+ private
35
+
36
+ def dionysus_insert_model_created
37
+ add_outbox_records_to_publish(Dionysus::Producer.outbox.insert_created(self))
38
+ end
39
+
40
+ def dionysus_insert_model_updated
41
+ add_outbox_records_to_publish(Dionysus::Producer.outbox.insert_updated(self))
42
+ end
43
+
44
+ def dionysus_insert_model_destroy
45
+ add_outbox_records_to_publish(Dionysus::Producer.outbox.insert_destroyed(self))
46
+ end
47
+
48
+ def add_outbox_records_to_publish(records)
49
+ self.class.add_outbox_records_to_publish(records)
50
+ end
51
+
52
+ def publish_outbox_records
53
+ begin
54
+ if publish_after_commit?
55
+ records_processor.call(
56
+ self.class.outbox_records_to_publish.sort_by(&:resource_created_at)
57
+ )
58
+ end
59
+ rescue => e
60
+ Dionysus.logger.error(
61
+ "[Dionysus Outbox from publish_outbox_records] #{e.class}: #{e}"
62
+ )
63
+ end
64
+ self.class.clear_records_to_publish
65
+ end
66
+
67
+ def records_processor
68
+ Dionysus::Producer::Outbox::RecordsProcessor.new
69
+ end
70
+
71
+ def publish_after_commit?
72
+ Dionysus::Producer.configuration.publish_after_commit
73
+ end
74
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Producer::Outbox::DatadogLatencyReporter
4
+ attr_reader :config
5
+ private :config
6
+
7
+ def initialize(config: Dionysus::Producer.configuration)
8
+ @config = config
9
+ end
10
+
11
+ def report(latency: generate_latency)
12
+ datadog_statsd_client.gauge("dionysus.producer.outbox.latency.minimum", latency.minimum)
13
+ datadog_statsd_client.gauge("dionysus.producer.outbox.latency.maximum", latency.maximum)
14
+ datadog_statsd_client.gauge("dionysus.producer.outbox.latency.average", latency.average)
15
+ datadog_statsd_client.gauge("dionysus.producer.outbox.latency.highest_since_creation_date",
16
+ latency.highest_since_creation_date)
17
+ end
18
+
19
+ private
20
+
21
+ delegate :datadog_statsd_client, :datadog_statsd_prefix, to: :config
22
+
23
+ def generate_latency
24
+ Dionysus::Producer::Outbox::LatencyTracker.new.calculate
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Producer::Outbox::DatadogLatencyReporterJob
4
+ include Sidekiq::Worker
5
+
6
+ sidekiq_options queue: Dionysus::Producer::Config.high_priority_sidekiq_queue
7
+
8
+ def perform
9
+ Dionysus::Producer::Outbox::DatadogLatencyReporter.new.report
10
+ end
11
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Producer::Outbox::DatadogLatencyReporterScheduler
4
+ JOB_NAME = "dionysus_producer_outbox_datadog_latency_reporter_job"
5
+ EVERY_MINUTE_IN_CRON_SYNTAX = "* * * * *"
6
+ JOB_CLASS_NAME = "Dionysus::Producer::Outbox::DatadogLatencyReporterJob"
7
+ JOB_DESCRIPTION = "Collect latency metrics from dionysus outbox and send them to Datadog"
8
+
9
+ private_constant :JOB_NAME, :EVERY_MINUTE_IN_CRON_SYNTAX, :JOB_CLASS_NAME, :JOB_DESCRIPTION
10
+
11
+ attr_reader :config
12
+ private :config
13
+
14
+ def initialize(config: Dionysus::Producer.configuration)
15
+ @config = config
16
+ end
17
+
18
+ def add_to_schedule
19
+ find || create
20
+ end
21
+
22
+ private
23
+
24
+ def find
25
+ Sidekiq::Cron::Job.find(name: JOB_NAME)
26
+ end
27
+
28
+ def create
29
+ Sidekiq::Cron::Job.create(create_job_arguments)
30
+ end
31
+
32
+ def create_job_arguments
33
+ {
34
+ name: JOB_NAME,
35
+ cron: EVERY_MINUTE_IN_CRON_SYNTAX,
36
+ class: JOB_CLASS_NAME,
37
+ queue: config.high_priority_sidekiq_queue,
38
+ active_job: false,
39
+ description: JOB_DESCRIPTION,
40
+ date_as_argument: false
41
+ }
42
+ end
43
+
44
+ def every_minute_to_cron_syntax
45
+ EVERY_MINUTE_IN_CRON_SYNTAX
46
+ end
47
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Producer::Outbox::DatadogTracer
4
+ SERVICE_NAME = "dionysus_outbox_worker"
5
+ private_constant :SERVICE_NAME
6
+
7
+ def self.service_name
8
+ SERVICE_NAME
9
+ end
10
+
11
+ def trace(event_name, topic)
12
+ tracer.trace(event_name, span_type: "worker", service: self.class.service_name, on_error: error_handler) do |span|
13
+ span.set_tag("topic", topic)
14
+
15
+ yield
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def tracer
22
+ if Datadog.respond_to?(:tracer)
23
+ Datadog.tracer
24
+ else
25
+ Datadog::Tracing
26
+ end
27
+ end
28
+
29
+ def error_handler
30
+ ->(span, error) { span.set_error(error) }
31
+ end
32
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Producer::Outbox::DuplicatesFilter
4
+ def self.call(records_to_publish)
5
+ new(records_to_publish).call
6
+ end
7
+
8
+ attr_reader :records_to_publish
9
+ private :records_to_publish
10
+
11
+ def initialize(records_to_publish)
12
+ @records_to_publish = records_to_publish
13
+ end
14
+
15
+ def call
16
+ records_to_publish
17
+ .slice_when { |record_1, record_2| generate_uniqueness_key(record_1) != generate_uniqueness_key(record_2) }
18
+ .flat_map(&:last)
19
+ end
20
+
21
+ private
22
+
23
+ def generate_uniqueness_key(record)
24
+ [record.resource_class, record.resource_id, record.event_name, record.topic]
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Producer::Outbox::EventName
4
+ attr_reader :resource_name
5
+ private :resource_name
6
+
7
+ def initialize(resource_name)
8
+ @resource_name = resource_name
9
+ end
10
+
11
+ def created
12
+ "#{resource_name}_created"
13
+ end
14
+
15
+ def updated
16
+ "#{resource_name}_updated"
17
+ end
18
+
19
+ def destroyed
20
+ "#{resource_name}_destroyed"
21
+ end
22
+
23
+ def for_event_type(type)
24
+ public_send(type)
25
+ end
26
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Producer::Outbox::HealthCheck
4
+ KEY_PREFIX = "__dionysus_outbox_worker__running__"
5
+ TMP_DIR = "/tmp"
6
+ private_constant :KEY_PREFIX, :TMP_DIR
7
+
8
+ def self.check(hostname: ENV.fetch("HOSTNAME", nil), expiry_time_in_seconds: 120)
9
+ new(hostname: hostname, expiry_time_in_seconds: expiry_time_in_seconds).check
10
+ end
11
+
12
+ attr_reader :hostname, :expiry_time_in_seconds
13
+
14
+ def initialize(hostname: ENV.fetch("HOSTNAME", nil), expiry_time_in_seconds: 120)
15
+ @hostname = hostname
16
+ @expiry_time_in_seconds = expiry_time_in_seconds
17
+ end
18
+
19
+ def check
20
+ if healthcheck_storage.running?
21
+ ""
22
+ else
23
+ "[Dionysus Producer Outbox healthcheck failed]"
24
+ end
25
+ end
26
+
27
+ def register_heartbeat
28
+ healthcheck_storage.touch
29
+ end
30
+
31
+ def worker_stopped
32
+ healthcheck_storage.remove
33
+ end
34
+
35
+ private
36
+
37
+ def healthcheck_storage
38
+ @healthcheck_storage ||= FileBasedHealthcheck.new(
39
+ directory: TMP_DIR,
40
+ filename: key,
41
+ time_threshold: expiry_time_in_seconds
42
+ )
43
+ end
44
+
45
+ def key
46
+ "#{KEY_PREFIX}#{hostname}"
47
+ end
48
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Producer::Outbox::LatencyTracker
4
+ LatencyTrackerResult = Struct.new(:minimum, :maximum, :average, :highest_since_creation_date)
5
+ private_constant :LatencyTrackerResult
6
+
7
+ attr_reader :config, :clock
8
+ private :config, :clock
9
+
10
+ def initialize(config: Dionysus::Producer.configuration, clock: Time)
11
+ @config = config
12
+ @clock = clock
13
+ end
14
+
15
+ def calculate(interval: 1.minute)
16
+ records = outbox_model.published_since(interval.ago)
17
+ latencies = records.map(&:publishing_latency)
18
+
19
+ LatencyTrackerResult.new(
20
+ latencies.min.to_d,
21
+ latencies.max.to_d,
22
+ calculate_average(latencies),
23
+ calculate_highest_since_creation_date.to_d
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ delegate :outbox_model, to: :config
30
+
31
+ def calculate_average(latencies)
32
+ if latencies.any?
33
+ latencies.sum.to_d / latencies.size
34
+ else
35
+ 0
36
+ end
37
+ end
38
+
39
+ def calculate_highest_since_creation_date
40
+ minimum_created_at_from_not_published = outbox_model.not_published.minimum(:created_at) or return 0
41
+ clock.current - minimum_created_at_from_not_published
42
+ end
43
+ end