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,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Producer::PartitionKey
4
+ attr_reader :resource, :config
5
+ private :resource, :config
6
+
7
+ def initialize(resource, config: Dionysus::Producer.configuration)
8
+ @resource = resource
9
+ @config = config
10
+ end
11
+
12
+ def to_key(responder:)
13
+ if has_custom_partition_key?(responder)
14
+ apply_custom_partition_key(responder)
15
+ else
16
+ apply_default_partition_key
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def has_custom_partition_key?(responder)
23
+ responder.partition_key.present?
24
+ end
25
+
26
+ def apply_custom_partition_key(responder)
27
+ apply_partition_key(responder.partition_key)
28
+ end
29
+
30
+ def apply_default_partition_key
31
+ apply_partition_key(config.default_partition_key)
32
+ end
33
+
34
+ def apply_partition_key(partition_key)
35
+ if partition_key.respond_to?(:call)
36
+ resolved_partition_key = partition_key.call(resource)
37
+ resolved_partition_key&.to_s
38
+ elsif resource.respond_to?(partition_key)
39
+ resource.public_send(partition_key).to_i.to_s
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Producer::Registry::Validator
4
+ attr_reader :registry
5
+ private :registry
6
+
7
+ def initialize(registry: Dionysus::Producer.registry)
8
+ @registry = registry
9
+ end
10
+
11
+ def validate_columns
12
+ registry.registrations.each_value do |registration|
13
+ registration.topics.each do |topic|
14
+ topic
15
+ .models
16
+ .flat_map(&:observables_config)
17
+ .each { |observable_config| validate_observable(observable_config) }
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def validate_observable(observable_config)
25
+ model = observable_config.fetch(:model)
26
+ attributes = observable_config.fetch(:attributes)
27
+
28
+ return if attributes.all? { |attribute| model.column_names.include?(attribute.to_s) }
29
+
30
+ raise ArgumentError.new("some attributes #{attributes} do not exist on model #{model}")
31
+ end
32
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Producer::Registry
4
+ attr_reader :container
5
+ private :container
6
+
7
+ def initialize
8
+ @container = {}
9
+ end
10
+
11
+ def namespace(namespace, &block)
12
+ registration = Registration.new(namespace)
13
+ registration.instance_eval(&block)
14
+ container[namespace] = registration
15
+ end
16
+
17
+ def registrations
18
+ container
19
+ end
20
+
21
+ class Registration
22
+ attr_reader :namespace, :topics, :serializer_klass, :producers
23
+
24
+ def initialize(namespace)
25
+ @namespace = namespace
26
+ @topics = []
27
+ @serializer_klass = nil
28
+ @producers = []
29
+ end
30
+
31
+ def serializer(serializer_klass)
32
+ @serializer_klass = serializer_klass
33
+ end
34
+
35
+ def topic(name, options = {}, &block)
36
+ new_topic = Topic.new(namespace, name, serializer_klass, options)
37
+ new_topic.instance_eval(&block)
38
+ producer = Dionysus::Producer::KarafkaResponderGenerator.new.generate(
39
+ Dionysus::Producer.configuration, new_topic
40
+ )
41
+ producers << producer
42
+ new_topic.producer = producer
43
+ topics << new_topic
44
+ end
45
+
46
+ class Topic
47
+ GENESIS_SUFFIX = "genesis"
48
+ private_constant :GENESIS_SUFFIX
49
+
50
+ attr_reader :namespace, :name, :serializer_klass, :options, :models
51
+
52
+ attr_accessor :producer
53
+
54
+ def initialize(namespace, name, serializer_klass, options = {})
55
+ @namespace = namespace
56
+ @name = name
57
+ @options = options
58
+ @serializer_klass = serializer_klass
59
+ @models = []
60
+ end
61
+
62
+ def to_s
63
+ Dionysus::TopicName.new(namespace, name).to_s
64
+ end
65
+
66
+ def genesis_to_s
67
+ Dionysus::TopicName.new(namespace, "#{name}_#{GENESIS_SUFFIX}").to_s if genesis_replica?
68
+ end
69
+
70
+ def partition_key
71
+ options.fetch(:partition_key, nil)
72
+ end
73
+
74
+ def genesis_replica?
75
+ options.fetch(:genesis_replica, false) == true
76
+ end
77
+
78
+ def publish(model_klass, model_registrations_options = {})
79
+ @models << ModelRegistration.new(model_klass, model_registrations_options)
80
+ end
81
+
82
+ def publishes_model?(model_klass)
83
+ models.any? { |registration| registration.model_klass == model_klass }
84
+ end
85
+
86
+ class ModelRegistration
87
+ attr_reader :model_klass, :options
88
+
89
+ def initialize(model_klass, options = {})
90
+ @model_klass = model_klass
91
+ @options = options
92
+ validate_and_set_up
93
+ end
94
+
95
+ def observes?(resource, changeset)
96
+ observer_config_for(resource, changeset).present?
97
+ end
98
+
99
+ def association_name_for_observable(resource, changeset)
100
+ observer_config_for(resource, changeset).fetch(:association_name)
101
+ end
102
+
103
+ def observables_config
104
+ options.fetch(:observe, [])
105
+ end
106
+
107
+ private
108
+
109
+ def validate_and_set_up
110
+ validate_options
111
+ set_up_as_publishables
112
+ end
113
+
114
+ def validate_options
115
+ return unless options.key?(:observe)
116
+
117
+ options[:observe].each do |observer_config|
118
+ model = observer_config.fetch(:model)
119
+ association_name = observer_config.fetch(:association_name)
120
+ _attributes = observer_config.fetch(:attributes)
121
+
122
+ if association_name.is_a?(Symbol) && !model.instance_methods.include?(association_name.to_sym)
123
+ raise ArgumentError.new("association name :#{association_name} does not exist on model #{model}")
124
+ end
125
+ end
126
+ end
127
+
128
+ def set_up_as_publishables
129
+ [model_klass, dependencies_from_parent, observables]
130
+ .flatten
131
+ .select { |model| model.instance_of?(Class) && model.ancestors.include?(ActiveRecord::Base) }
132
+ .each { |model| ensure_model_is_publishable(model) }
133
+ end
134
+
135
+ def dependencies_from_parent
136
+ options.fetch(:with, [])
137
+ end
138
+
139
+ def observables
140
+ options.fetch(:observe, []).map { |hash| hash.fetch(:model) }
141
+ end
142
+
143
+ def ensure_model_is_publishable(model)
144
+ model.include(publishable_module) unless model.included_modules.include?(publishable_module)
145
+ end
146
+
147
+ def publishable_module
148
+ Dionysus::Producer::Outbox::ActiveRecordPublishable
149
+ end
150
+
151
+ def observer_config_for(resource, changeset)
152
+ resource_model_name = resource.model_name.to_s
153
+ changeset_attributes = changeset.keys.map(&:to_sym)
154
+
155
+ options[:observe].to_a.find do |observer_config|
156
+ config_model_name = observer_config.fetch(:model).to_s
157
+ config_attributes = observer_config.fetch(:attributes).to_a.map(&:to_sym)
158
+
159
+ config_model_name == resource_model_name && (config_attributes & changeset_attributes).any?
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Producer::Serializer
4
+ def self.serialize(record_or_records, dependencies: [])
5
+ new(record_or_records, dependencies: dependencies).serialize
6
+ end
7
+
8
+ attr_reader :records, :dependencies
9
+ private :records, :dependencies
10
+
11
+ def initialize(record_or_records, dependencies: [])
12
+ @records = Array.wrap(record_or_records).compact
13
+
14
+ @dependencies = dependencies
15
+ end
16
+
17
+ def serialize
18
+ records.map do |record|
19
+ serializer = resolve_serializer_for_record(record)
20
+ serializer.as_json
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def resolve_serializer_for_record(record)
27
+ if record.persisted?
28
+ infer_serializer.new(record, include: include, context_serializer: self.class)
29
+ else
30
+ deleted_record_serializer(record)
31
+ end
32
+ end
33
+
34
+ def infer_serializer
35
+ raise "implement me!"
36
+ end
37
+
38
+ def deleted_record_serializer(record)
39
+ Dionysus::Producer::DeletedRecordSerializer.new(record, include: include,
40
+ context_serializer: self.class)
41
+ end
42
+
43
+ def model_klass
44
+ records.first&.class
45
+ end
46
+
47
+ def include
48
+ dependencies.to_a.map do |model_klass|
49
+ [model_klass.model_name.plural.to_sym, model_klass.model_name.singular.to_sym]
50
+ end.flatten
51
+ end
52
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Producer::Suppressor
4
+ SUPPRESSION_KEY = :dionysus_producer_suppressed
5
+ private_constant :SUPPRESSION_KEY
6
+
7
+ def self.suppressed?
8
+ Thread.current[SUPPRESSION_KEY] == true
9
+ end
10
+
11
+ def self.suppress!
12
+ Thread.current[SUPPRESSION_KEY] = true
13
+ end
14
+
15
+ def self.unsuppress!
16
+ Thread.current[SUPPRESSION_KEY] = nil
17
+ end
18
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Producer
4
+ def self.configuration
5
+ @configuration ||= Dionysus::Producer::Config.new
6
+ end
7
+
8
+ def self.configure
9
+ yield configuration
10
+ end
11
+
12
+ def self.registry
13
+ configuration.registry
14
+ end
15
+
16
+ def self.declare(&config)
17
+ registry = Dionysus::Producer::Registry.new
18
+
19
+ registry.instance_eval(&config)
20
+ configure do |configuration|
21
+ configuration.registry = registry
22
+ end
23
+ end
24
+
25
+ def self.outbox
26
+ Dionysus::Producer::Outbox.new(configuration.outbox_model, config: configuration)
27
+ end
28
+
29
+ def self.outbox_publisher
30
+ Dionysus::Producer::Outbox::Publisher.new(config: configuration)
31
+ end
32
+
33
+ def self.reset!
34
+ return if registry.nil?
35
+
36
+ registry.registrations.values.flat_map(&:producers).each do |producer_class|
37
+ Dionysus.send(:remove_const, producer_class.name.demodulize.to_sym) if producer_class.name
38
+ end
39
+ @configuration = Dionysus::Producer::Config.new
40
+ end
41
+
42
+ def self.responders_for(model_klass)
43
+ return [] if registry.nil?
44
+
45
+ registry.registrations.each.with_object([]) do |(_, registration), responders|
46
+ registration.producers.select { |producer| producer.publisher_of?(model_klass) }.each do |producer|
47
+ responders << producer
48
+ end
49
+ end
50
+ end
51
+
52
+ def self.responders_for_model_for_topic(model_klass, topic)
53
+ responders_for(model_klass).select { |responder| responder.publisher_of_model_for_topic?(model_klass, topic) }
54
+ end
55
+
56
+ def self.responders_for_dependency_parent(model_klass)
57
+ return [] if registry.nil?
58
+
59
+ registry.registrations.values.each.with_object([]) do |registration, accum|
60
+ registration.topics.each do |topic|
61
+ topic
62
+ .models
63
+ .select { |model_registration| model_registration.options[:with].to_a.include?(model_klass) }
64
+ .each do |model_registration|
65
+ accum << [model_registration.model_klass, topic.producer]
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ def self.responders_for_dependency_parent_for_topic(model_klass, topic)
72
+ responders_for_dependency_parent(model_klass).select do |_model, responder|
73
+ responder.publisher_for_topic?(topic)
74
+ end
75
+ end
76
+
77
+ def self.start_outbox_worker(threads_number:)
78
+ runners = (1..threads_number).map do
79
+ Dionysus::Producer::Outbox::Runner.new(config: configuration)
80
+ end
81
+ executor = Sigurd::Executor.new(runners, sleep_seconds: 5, logger: Dionysus.logger)
82
+ signal_handler = Sigurd::SignalHandler.new(executor)
83
+ signal_handler.run!
84
+ end
85
+
86
+ def self.topics_models_mapping
87
+ return {} if registry.nil?
88
+
89
+ registry
90
+ .registrations
91
+ .values
92
+ .flat_map(&:topics)
93
+ .to_h do |topic|
94
+ [
95
+ topic.to_s,
96
+ topic.models.to_h { |registration| [registration.model_klass, registration.options.fetch(:with, [])] }
97
+ ]
98
+ end
99
+ end
100
+
101
+ def self.observers_with_responders_for(resource, changeset)
102
+ return [] if registry.nil?
103
+
104
+ registry.registrations.values.each.with_object([]) do |registration, accum|
105
+ registration.topics.each do |topic|
106
+ topic
107
+ .models
108
+ .select { |model_registration| model_registration.observes?(resource, changeset) }
109
+ .each do |model_registration|
110
+ association_name = model_registration.association_name_for_observable(resource, changeset)
111
+ methods_chain = association_name.to_s.split(".")
112
+ association_or_associations = methods_chain.inject(resource) do |record, method_name|
113
+ record.public_send(method_name)
114
+ end
115
+
116
+ accum << [Array.wrap(association_or_associations).compact, topic.producer]
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(Rails)
4
+ class Dionysus::Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load "tasks/dionysus.rake"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dionysus::Rb
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rb/version"
4
+
5
+ # Your code goes here...
6
+ class Dionysus::Rb::Error < StandardError
7
+ # Your code goes here...
8
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples_for "Dionysus Transactional Outbox Publishable" do |extra_attributes|
4
+ let(:attributes_for_model) { extra_attributes.to_h }
5
+
6
+ describe "Dionysus Transactional Outbox" do
7
+ let(:outbox_model) { Dionysus::Producer.configuration.outbox_model }
8
+
9
+ before do
10
+ outbox_model.delete_all
11
+ end
12
+
13
+ context "when the record gets created" do
14
+ subject(:create_record) { create(described_class.model_name.singular.to_sym, attributes_for_model) }
15
+
16
+ let(:resource_class) { described_class.model_name.to_s }
17
+ let(:resource_id) { create_record.id }
18
+ let(:event_name) { "#{described_class.model_name.singular}_created" }
19
+ let(:outbox_records_for_resource) do
20
+ outbox_model.where(resource_class: resource_class, event_name: event_name)
21
+ end
22
+ let(:outbox_record_for_created_resource) do
23
+ outbox_model.find_by(resource_class: resource_class, resource_id: resource_id, event_name: event_name)
24
+ end
25
+
26
+ it "creates an Outbox Record" do
27
+ expect do
28
+ create_record
29
+ end.to change { outbox_records_for_resource.count }.from(0)
30
+ expect(outbox_record_for_created_resource).to be_present
31
+ end
32
+ end
33
+
34
+ context "when the record gets updated" do
35
+ subject(:update_record) { record.update(attributes_for_model.merge(updated_at: Time.current)) }
36
+
37
+ let!(:record) { create(described_class.model_name.singular.to_sym) }
38
+ let(:resource_class) { described_class.model_name.to_s }
39
+ let(:resource_id) { record.id }
40
+ let(:event_name) { "#{described_class.model_name.singular}_updated" }
41
+ let(:outbox_records_for_resource) do
42
+ outbox_model.where(resource_class: resource_class, event_name: event_name)
43
+ end
44
+ let(:outbox_record_for_updated_resource) do
45
+ outbox_model.find_by(resource_class: resource_class, resource_id: resource_id, event_name: event_name)
46
+ end
47
+
48
+ it "creates an Outbox Record" do
49
+ expect do
50
+ update_record
51
+ end.to change { outbox_records_for_resource.count }.from(0)
52
+ expect(outbox_record_for_updated_resource).to be_present
53
+ end
54
+ end
55
+
56
+ context "when the record gets deleted" do
57
+ subject(:delete_record) { record.destroy }
58
+
59
+ let!(:record) { create(described_class.model_name.singular.to_sym) }
60
+ let(:resource_class) { described_class.model_name.to_s }
61
+ let(:resource_id) { record.id }
62
+ let(:event_name) { "#{described_class.model_name.singular}_destroyed" }
63
+ let(:outbox_records_for_resource) do
64
+ outbox_model.where(resource_class: resource_class, event_name: event_name)
65
+ end
66
+ let(:outbox_record_for_deleted_resource) do
67
+ outbox_model.find_by(resource_class: resource_class, resource_id: resource_id, event_name: event_name)
68
+ end
69
+
70
+ it "creates an Outbox Record" do
71
+ expect do
72
+ delete_record
73
+ end.to change { outbox_records_for_resource.count }.from(0)
74
+ expect(outbox_record_for_deleted_resource).to be_present
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::TopicName
4
+ attr_reader :namespace, :name
5
+ private :namespace, :name
6
+
7
+ def initialize(namespace, name)
8
+ @namespace = namespace
9
+ @name = name
10
+ end
11
+
12
+ def to_s
13
+ "#{namespace}_#{name}"
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Utils::DefaultMessageFilter
4
+ attr_reader :error_handler
5
+ private :error_handler
6
+
7
+ def initialize(error_handler:)
8
+ @error_handler = error_handler
9
+ end
10
+
11
+ def ignore_message?(topic:, message:, transformed_data:)
12
+ false
13
+ end
14
+
15
+ def notify_about_ignored_message(topic:, message:, transformed_data:)
16
+ error_handler.capture_message(error_message(topic, message, transformed_data))
17
+ end
18
+
19
+ private
20
+
21
+ def error_message(topic, message, transformed_data)
22
+ "Ignoring Kafka message. Make sure it's processed later (e.g. by directly doing it from console): " \
23
+ "topic: #{topic}, message: #{message}, transformed_data: #{transformed_data}."
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Utils::ExponentialBackoff
4
+ def self.backoff_for(multiplier, count)
5
+ (multiplier * (2**count))
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Utils::KarafkaDatadogListener
4
+ class << self
5
+ def on_error_occurred(event)
6
+ span = tracer.active_span
7
+ span&.set_error(event[:error])
8
+ end
9
+
10
+ private
11
+
12
+ def tracer
13
+ if Datadog.respond_to?(:tracer)
14
+ Datadog.tracer
15
+ else
16
+ Datadog::Tracing
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dionysus::Utils::KarafkaSentryListener
4
+ class << self
5
+ def on_error_occurred(event)
6
+ Sentry.capture_exception(event[:error])
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Utils::NullErrorHandler
4
+ def self.capture_exception(_error); end
5
+ def self.capture_message(_error); end
6
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Utils::NullEventBus
4
+ def self.publish(_name, _payload); end
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Utils::NullHermesEventProducer
4
+ def self.publish(*); end
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Utils::NullInstrumenter
4
+ def self.instrument(_name, _payload = {})
5
+ yield
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Utils::NullLockClient
4
+ def self.lock(resource_key, expiration_time)
5
+ payload = {
6
+ validity: expiration_time,
7
+ resource: resource_key,
8
+ value: "null_lock_client_lock"
9
+ }
10
+
11
+ yield payload
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Utils::NullModelFactory
4
+ def self.for_model(_model_name); end
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Utils::NullMutexProvider
4
+ def self.with_lock(_name)
5
+ yield
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Utils::NullRetryProvider
4
+ def self.retry
5
+ yield
6
+ end
7
+ end