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