dionysus-rb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +61 -0
- data/.github/workflows/ci.yml +77 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +175 -0
- data/.rubocop_todo.yml +53 -0
- data/CHANGELOG.md +227 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +258 -0
- data/LICENSE.txt +21 -0
- data/README.md +1206 -0
- data/Rakefile +10 -0
- data/assets/logo.svg +51 -0
- data/bin/console +11 -0
- data/bin/karafka_health_check +14 -0
- data/bin/outbox_worker_health_check +12 -0
- data/bin/setup +8 -0
- data/dionysus-rb.gemspec +64 -0
- data/docker-compose.yml +44 -0
- data/lib/dionysus/checks/health_check.rb +50 -0
- data/lib/dionysus/checks.rb +7 -0
- data/lib/dionysus/consumer/batch_events_publisher.rb +33 -0
- data/lib/dionysus/consumer/config.rb +97 -0
- data/lib/dionysus/consumer/deserializer.rb +231 -0
- data/lib/dionysus/consumer/dionysus_event.rb +42 -0
- data/lib/dionysus/consumer/karafka_consumer_generator.rb +56 -0
- data/lib/dionysus/consumer/params_batch_processor.rb +65 -0
- data/lib/dionysus/consumer/params_batch_transformations/remove_duplicates_strategy.rb +54 -0
- data/lib/dionysus/consumer/params_batch_transformations.rb +4 -0
- data/lib/dionysus/consumer/persistor.rb +157 -0
- data/lib/dionysus/consumer/registry.rb +84 -0
- data/lib/dionysus/consumer/synced_data/assign_columns_from_synced_data.rb +27 -0
- data/lib/dionysus/consumer/synced_data/assign_columns_from_synced_data_job.rb +26 -0
- data/lib/dionysus/consumer/synced_data.rb +4 -0
- data/lib/dionysus/consumer/synchronizable_model.rb +93 -0
- data/lib/dionysus/consumer/workers_group.rb +18 -0
- data/lib/dionysus/consumer.rb +36 -0
- data/lib/dionysus/monitor.rb +48 -0
- data/lib/dionysus/producer/base_responder.rb +46 -0
- data/lib/dionysus/producer/config.rb +104 -0
- data/lib/dionysus/producer/deleted_record_serializer.rb +17 -0
- data/lib/dionysus/producer/genesis/performed.rb +11 -0
- data/lib/dionysus/producer/genesis/stream_job.rb +13 -0
- data/lib/dionysus/producer/genesis/streamer/base_job.rb +44 -0
- data/lib/dionysus/producer/genesis/streamer/standard_job.rb +43 -0
- data/lib/dionysus/producer/genesis/streamer.rb +40 -0
- data/lib/dionysus/producer/genesis.rb +62 -0
- data/lib/dionysus/producer/karafka_responder_generator.rb +133 -0
- data/lib/dionysus/producer/key.rb +14 -0
- data/lib/dionysus/producer/model_serializer.rb +105 -0
- data/lib/dionysus/producer/outbox/active_record_publishable.rb +74 -0
- data/lib/dionysus/producer/outbox/datadog_latency_reporter.rb +26 -0
- data/lib/dionysus/producer/outbox/datadog_latency_reporter_job.rb +11 -0
- data/lib/dionysus/producer/outbox/datadog_latency_reporter_scheduler.rb +47 -0
- data/lib/dionysus/producer/outbox/datadog_tracer.rb +32 -0
- data/lib/dionysus/producer/outbox/duplicates_filter.rb +26 -0
- data/lib/dionysus/producer/outbox/event_name.rb +26 -0
- data/lib/dionysus/producer/outbox/health_check.rb +48 -0
- data/lib/dionysus/producer/outbox/latency_tracker.rb +43 -0
- data/lib/dionysus/producer/outbox/model.rb +117 -0
- data/lib/dionysus/producer/outbox/producer.rb +26 -0
- data/lib/dionysus/producer/outbox/publishable.rb +106 -0
- data/lib/dionysus/producer/outbox/publisher.rb +131 -0
- data/lib/dionysus/producer/outbox/records_processor.rb +56 -0
- data/lib/dionysus/producer/outbox/runner.rb +120 -0
- data/lib/dionysus/producer/outbox/tombstone_publisher.rb +22 -0
- data/lib/dionysus/producer/outbox.rb +103 -0
- data/lib/dionysus/producer/partition_key.rb +42 -0
- data/lib/dionysus/producer/registry/validator.rb +32 -0
- data/lib/dionysus/producer/registry.rb +165 -0
- data/lib/dionysus/producer/serializer.rb +52 -0
- data/lib/dionysus/producer/suppressor.rb +18 -0
- data/lib/dionysus/producer.rb +121 -0
- data/lib/dionysus/railtie.rb +9 -0
- data/lib/dionysus/rb/version.rb +5 -0
- data/lib/dionysus/rb.rb +8 -0
- data/lib/dionysus/support/rspec/outbox_publishable.rb +78 -0
- data/lib/dionysus/topic_name.rb +15 -0
- data/lib/dionysus/utils/default_message_filter.rb +25 -0
- data/lib/dionysus/utils/exponential_backoff.rb +7 -0
- data/lib/dionysus/utils/karafka_datadog_listener.rb +20 -0
- data/lib/dionysus/utils/karafka_sentry_listener.rb +9 -0
- data/lib/dionysus/utils/null_error_handler.rb +6 -0
- data/lib/dionysus/utils/null_event_bus.rb +5 -0
- data/lib/dionysus/utils/null_hermes_event_producer.rb +5 -0
- data/lib/dionysus/utils/null_instrumenter.rb +7 -0
- data/lib/dionysus/utils/null_lock_client.rb +13 -0
- data/lib/dionysus/utils/null_model_factory.rb +5 -0
- data/lib/dionysus/utils/null_mutex_provider.rb +7 -0
- data/lib/dionysus/utils/null_retry_provider.rb +7 -0
- data/lib/dionysus/utils/null_tracer.rb +5 -0
- data/lib/dionysus/utils/null_transaction_provider.rb +15 -0
- data/lib/dionysus/utils/sidekiq_batched_job_distributor.rb +24 -0
- data/lib/dionysus/utils.rb +6 -0
- data/lib/dionysus/version.rb +7 -0
- data/lib/dionysus-rb.rb +3 -0
- data/lib/dionysus.rb +133 -0
- data/lib/tasks/dionysus.rake +18 -0
- data/log/development.log +0 -0
- data/sig/dionysus/rb.rbs +6 -0
- 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,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
|