dionysus-rb 0.1.0

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.
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