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,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/array/wrap"
4
+
5
+ class Dionysus::Consumer::Deserializer
6
+ attr_reader :data
7
+
8
+ def initialize(data)
9
+ @data = data.to_a
10
+ end
11
+
12
+ def deserialize
13
+ data.map { |serialized_payload| SerializedRecord.new(serialized_payload) }.map do |serialized_record|
14
+ transformed_payload = DeserializedRecord.new
15
+ transformed_payload = populate_attributes_with_relationships(serialized_record, transformed_payload)
16
+
17
+ transformed_payload = assign_attributes(serialized_record, transformed_payload)
18
+
19
+ serialized_record.expected_has_many_relationships.each do |relationship_name|
20
+ deserialize_has_many_relationship(serialized_record, relationship_name, transformed_payload)
21
+ end
22
+
23
+ serialized_record.expected_has_one_relationships.each do |relationship_name|
24
+ deserialize_has_one_relationship(serialized_record, relationship_name, transformed_payload)
25
+ end
26
+
27
+ transformed_payload
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def populate_attributes_with_relationships(serialized_record, transformed_payload)
34
+ serialized_record.links.each do |relationship_name, value|
35
+ transformed_payload.populate_attributes_with_relationship(relationship_name, value)
36
+ end
37
+
38
+ transformed_payload
39
+ end
40
+
41
+ def assign_attributes(serialized_record, transformed_payload)
42
+ transformed_payload.synced_id = serialized_record.id
43
+ transformed_payload.synced_created_at = serialized_record.created_at if serialized_record.has_created_at?
44
+ transformed_payload.synced_updated_at = serialized_record.updated_at if serialized_record.has_updated_at?
45
+ transformed_payload.synced_canceled_at = serialized_record.canceled_at if serialized_record.has_canceled_at?
46
+
47
+ serialized_record.plain_attributes.each do |attribute, value|
48
+ transformed_payload.attributes[attribute] = value
49
+ end
50
+
51
+ transformed_payload
52
+ end
53
+
54
+ def deserialize_has_many_relationship(serialized_record, relationship_name, transformed_payload)
55
+ value = serialized_record[relationship_name]
56
+ deserialized_relationship = (value && Dionysus::Consumer::Deserializer.new(value).deserialize) || nil
57
+ relationship_model_name = serialized_record.model_name_for_relationship(relationship_name)
58
+ transformed_payload.has_many << [relationship_model_name, deserialized_relationship]
59
+ transformed_payload.delete(relationship_name)
60
+ transformed_payload
61
+ end
62
+
63
+ def deserialize_has_one_relationship(serialized_record, relationship_name, transformed_payload)
64
+ value = serialized_record[relationship_name]
65
+ deserialized_relationship = Dionysus::Consumer::Deserializer.new(Array.wrap(value)).deserialize
66
+ relationship_model_name = serialized_record.model_name_for_relationship(relationship_name)
67
+ transformed_payload.has_one << [relationship_model_name, deserialized_relationship.first]
68
+ transformed_payload.delete(relationship_name)
69
+ transformed_payload
70
+ end
71
+
72
+ class DeserializedRecord < SimpleDelegator
73
+ def initialize
74
+ super(canonical_format)
75
+ end
76
+
77
+ def transformed_payload
78
+ __getobj__
79
+ end
80
+
81
+ def attributes
82
+ transformed_payload.fetch(:attributes)
83
+ end
84
+
85
+ def has_many
86
+ transformed_payload.fetch(:has_many)
87
+ end
88
+
89
+ def has_one
90
+ transformed_payload.fetch(:has_one)
91
+ end
92
+
93
+ def synced_id
94
+ attributes.fetch("synced_id") { "synced_id not found in #{attributes}! Something is very wrong." }
95
+ end
96
+
97
+ def synced_id=(val)
98
+ attributes["synced_id"] = val
99
+ end
100
+
101
+ def synced_created_at
102
+ attributes["synced_created_at"]
103
+ end
104
+
105
+ def synced_created_at=(val)
106
+ attributes["synced_created_at"] = val
107
+ end
108
+
109
+ def synced_updated_at
110
+ attributes["synced_updated_at"]
111
+ end
112
+
113
+ def synced_updated_at=(val)
114
+ attributes["synced_updated_at"] = val
115
+ end
116
+
117
+ def synced_canceled_at
118
+ attributes["synced_canceled_at"]
119
+ end
120
+
121
+ def synced_canceled_at=(val)
122
+ attributes["synced_canceled_at"] = val
123
+ end
124
+
125
+ def populate_attributes_with_relationship(relationship_name, value)
126
+ if value.respond_to?(:to_hash)
127
+ attributes["synced_#{relationship_name}_id"] = value["id"]
128
+ attributes["synced_#{relationship_name}_type"] = value["type"]
129
+ elsif value.respond_to?(:to_ary)
130
+ relationship_name = ActiveSupport::Inflector.singularize(relationship_name)
131
+ attributes["synced_#{relationship_name}_ids"] = value
132
+ else
133
+ attributes["synced_#{relationship_name}_id"] = value
134
+ end
135
+ end
136
+
137
+ def has_synced_canceled_at?
138
+ attributes.key?("synced_canceled_at")
139
+ end
140
+
141
+ private
142
+
143
+ def canonical_format
144
+ { attributes: {}, has_many: [], has_one: [] }
145
+ end
146
+ end
147
+
148
+ class SerializedRecord
149
+ RESERVED_ATTRIBUTES = %w[links id created_at updated_at canceled_at].freeze
150
+ private_constant :RESERVED_ATTRIBUTES
151
+
152
+ attr_reader :payload
153
+ private :payload
154
+
155
+ delegate :key?, :[], to: :payload
156
+
157
+ def initialize(payload)
158
+ @payload = payload
159
+ end
160
+
161
+ def plain_attributes
162
+ payload.except(*RESERVED_ATTRIBUTES, *expected_has_many_relationships, *expected_has_one_relationships)
163
+ end
164
+
165
+ def expected_has_many_relationships
166
+ to_many_foreign_keys.keys
167
+ end
168
+
169
+ def expected_has_one_relationships
170
+ to_one_foreign_keys.keys
171
+ end
172
+
173
+ def model_name_for_relationship(relationship_name)
174
+ if polymorphic_relationship?(relationship_name) && links[relationship_name].key?("type")
175
+ links[relationship_name]["type"]
176
+ else
177
+ relationship_name
178
+ end
179
+ end
180
+
181
+ def id
182
+ payload["id"]
183
+ end
184
+
185
+ def created_at
186
+ payload["created_at"]
187
+ end
188
+
189
+ def updated_at
190
+ payload["updated_at"]
191
+ end
192
+
193
+ def canceled_at
194
+ payload["canceled_at"]
195
+ end
196
+
197
+ def has?(key)
198
+ key?(key)
199
+ end
200
+
201
+ def has_created_at?
202
+ has?("created_at")
203
+ end
204
+
205
+ def has_updated_at?
206
+ has?("updated_at")
207
+ end
208
+
209
+ def has_canceled_at?
210
+ has?("canceled_at")
211
+ end
212
+
213
+ def links
214
+ payload["links"].to_h
215
+ end
216
+
217
+ private
218
+
219
+ def to_one_foreign_keys
220
+ links.reject { |_, value| value.respond_to?(:to_ary) }
221
+ end
222
+
223
+ def to_many_foreign_keys
224
+ links.select { |_, value| value.respond_to?(:to_ary) }
225
+ end
226
+
227
+ def polymorphic_relationship?(relationship_name)
228
+ links[relationship_name]&.respond_to?(:to_hash)
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Consumer::DionysusEvent
4
+ attr_reader :event_name, :model_name, :transformed_data, :local_changes
5
+
6
+ def initialize(event_name, model_name, transformed_data, aggregate_root: true)
7
+ @event_name = event_name.to_s
8
+ @model_name = model_name.to_s
9
+ @transformed_data = transformed_data
10
+ @local_changes = {}
11
+ @aggregate_root = aggregate_root
12
+ end
13
+
14
+ def created?
15
+ event_name.end_with?("created")
16
+ end
17
+
18
+ def updated?
19
+ event_name.end_with?("updated")
20
+ end
21
+
22
+ def destroyed?
23
+ event_name.end_with?("destroyed")
24
+ end
25
+
26
+ def generic_event?
27
+ created? || updated? || destroyed?
28
+ end
29
+
30
+ def aggregate_root?
31
+ @aggregate_root == true
32
+ end
33
+
34
+ def to_h
35
+ {
36
+ event_name: event_name,
37
+ model_name: model_name,
38
+ transformed_data: transformed_data,
39
+ local_changes: local_changes
40
+ }
41
+ end
42
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Consumer::KarafkaConsumerGenerator
4
+ TOMBSTONE = nil
5
+
6
+ def generate(config, topic)
7
+ base_class = topic.consumer_base_class || config.consumer_base_class
8
+
9
+ consumer_klass = Class.new(base_class) do
10
+ define_method :consume do
11
+ config.retry_provider.retry do
12
+ processed_events = Concurrent::Array.new
13
+ config.instrumenter.instrument("dionysus.consume.#{topic}") do
14
+ batch_number = 0
15
+
16
+ if topic.concurrency
17
+ workers = Dionysus::Consumer::WorkersGroup.new
18
+ messages.each do |batch|
19
+ batch_number += 1 # cannot use each_with_index on params_batch
20
+ worker = Thread.new do
21
+ Thread.current.report_on_exception = true
22
+ Thread.current.abort_on_exception = true
23
+ processed_events.concat(process_batch(config, topic, batch, batch_number))
24
+ end
25
+ workers << worker
26
+ end
27
+ workers.work
28
+ else
29
+ final_params_batch = topic.params_batch_transformation&.call(messages) || messages
30
+ final_params_batch.each do |batch|
31
+ batch_number += 1 # cannot use each_with_index on params_batch
32
+ processed_events.concat(process_batch(config, topic, batch, batch_number))
33
+ end
34
+ end
35
+ end
36
+ Dionysus::Consumer::BatchEventsPublisher.new(config, topic).publish(processed_events)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ define_method :process_batch do |configuration, current_topic, batch, batch_number|
43
+ configuration.transaction_provider.transaction do
44
+ Dionysus::Consumer::ParamsBatchProcessor.new(configuration, current_topic).process(batch,
45
+ batch_number)
46
+ end
47
+ end
48
+ end
49
+
50
+ consumer_klass_name = "#{topic.to_s.classify}Consumer"
51
+
52
+ Dionysus.send(:remove_const, consumer_klass_name) if Dionysus.const_defined?(consumer_klass_name)
53
+ Dionysus.const_set(consumer_klass_name, consumer_klass)
54
+ consumer_klass
55
+ end
56
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Consumer::ParamsBatchProcessor
4
+ attr_reader :config, :topic
5
+ private :config, :topic
6
+
7
+ def initialize(config, topic)
8
+ @config = config
9
+ @topic = topic
10
+ end
11
+
12
+ def process(batch, batch_number)
13
+ processed_events = []
14
+ instrument("dionysus.consume.#{topic}.batch_number_#{batch_number}") do
15
+ payload = batch.payload.to_h
16
+ metadata = batch.metadata
17
+ message = payload["message"].to_a
18
+
19
+ with_mutex(metadata, config) do
20
+ message.each do |current_event|
21
+ event_name = current_event["event"]
22
+ data = Array.wrap(current_event["data"])
23
+ model_name = current_event["model_name"]
24
+
25
+ transformed_data = nil
26
+ config.instrumenter.instrument("dionysus.consume.#{topic}.batch_number_#{batch_number}.deserialize") do
27
+ transformed_data = Dionysus::Consumer::Deserializer.new(data).deserialize
28
+ end
29
+
30
+ if ignore_message?(topic: topic, message: message, transformed_data: transformed_data)
31
+ notify_about_ignored_message(topic: topic, message: message, transformed_data: transformed_data)
32
+ next
33
+ end
34
+
35
+ dionysus_event = Dionysus::Consumer::DionysusEvent.new(event_name, model_name,
36
+ transformed_data)
37
+ config.instrumenter.instrument("dionysus.consume.#{topic}.batch_number_#{batch_number}.persist",
38
+ dionysus_event.to_h.except(:transformed_data, :local_changes)) do
39
+ config.transaction_provider.connection_pool.with_connection do
40
+ Dionysus::Consumer::Persistor.new(config, topic).persist(dionysus_event,
41
+ batch_number)
42
+ end
43
+ end
44
+ processed_events << dionysus_event
45
+ end
46
+ end
47
+ end
48
+ processed_events
49
+ end
50
+
51
+ private
52
+
53
+ delegate :message_filter, to: :config
54
+ delegate :ignore_message?, :notify_about_ignored_message, to: :message_filter
55
+
56
+ def instrument(label, options = {}, &block)
57
+ config.instrumenter.instrument(label, options, &block)
58
+ end
59
+
60
+ def with_mutex(metadata, config, &block)
61
+ message_key = metadata.key || SecureRandom.uuid
62
+ config.processing_mutex_provider.send(config.processing_mutex_method_name, "Dionysus-#{message_key}",
63
+ &block)
64
+ end
65
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Consumer::ParamsBatchTransformations::RemoveDuplicatesStrategy
4
+ def call(params_batch)
5
+ return params_batch if duplicates_removal_not_applicable?(params_batch)
6
+
7
+ Karafka::Messages::Messages.new(transform_messages_array(params_batch), params_batch.metadata)
8
+ end
9
+
10
+ private
11
+
12
+ # the idea is following:
13
+ # 1. group messages by event and id - for a given model we can expect unique messages for _created and _deleted ecents
14
+ # but we can have multiple _updated events, so this is where we are interested in removing duplicates
15
+ # 2. sort each group by updated_at, reverse (because the sort is ascending) and take the first one -
16
+ # this way we will have the most recent update
17
+ # 3. flatten the array of arrays as all groups will have a single item
18
+ # It is safer to apply sorting just for the _updated events to a given model as otherwise we could change the order of
19
+ # the messages for a different type which might be not desirable
20
+ def transform_messages_array(params_batch)
21
+ params_batch
22
+ .to_a
23
+ .group_by { |batch| grouping_key_by_event_and_id(batch) }
24
+ .map { |_, group| group.max_by { |batch| updated_at_from_batch(batch) } }
25
+ .flatten
26
+ end
27
+
28
+ def grouping_key_by_event_and_id(batch)
29
+ [
30
+ batch.payload.fetch("message").first.fetch("event"),
31
+ batch.payload.fetch("message").first["data"].first.fetch("id", nil)
32
+ ].join
33
+ end
34
+
35
+ def updated_at_from_batch(batch)
36
+ timestamp = batch.payload.fetch("message", []).first.to_h.fetch("data", []).first.to_h.fetch("updated_at", nil)
37
+
38
+ return timestamp.to_datetime if timestamp.respond_to?(:to_datetime)
39
+
40
+ Time.current
41
+ end
42
+
43
+ def duplicates_removal_not_applicable?(params_batch)
44
+ any_message_containing_more_than_one_event?(params_batch) || any_event_containing_more_than_one_item(params_batch)
45
+ end
46
+
47
+ def any_message_containing_more_than_one_event?(params_batch)
48
+ params_batch.any? { |batch| batch.payload.to_h.fetch("message", []).size != 1 }
49
+ end
50
+
51
+ def any_event_containing_more_than_one_item(params_batch)
52
+ params_batch.any? { |batch| batch.payload.to_h.fetch("message", []).first.to_h.fetch("data", []).size != 1 }
53
+ end
54
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Consumer::ParamsBatchTransformations
4
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Consumer::Persistor
4
+ attr_reader :config, :topic
5
+ private :config, :topic
6
+
7
+ def initialize(config, topic)
8
+ @config = config
9
+ @topic = topic
10
+ end
11
+
12
+ def persist(dionysus_event, batch_number)
13
+ if dionysus_event.generic_event?
14
+ if dionysus_event.created? && topic.options[:import] == true
15
+ persist_via_dionysus_create(dionysus_event, batch_number)
16
+ elsif dionysus_event.destroyed? && topic.options[:import] == true
17
+ persist_via_dionysus_destroy(dionysus_event, batch_number)
18
+ else
19
+ persist_standard_event(dionysus_event, batch_number)
20
+ end
21
+ else
22
+ log_unknown_event_type(dionysus_event)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def persist_via_dionysus_create(dionysus_event, batch_number)
29
+ model_klass = find_model_klass(dionysus_event) or return
30
+ config.instrumenter.instrument("dionysus.consume.#{topic}.batch_number_#{batch_number}.persist.dionysus_import") do
31
+ model_klass.dionysus_import(dionysus_event.transformed_data)
32
+ end
33
+ end
34
+
35
+ def persist_via_dionysus_destroy(dionysus_event, batch_number)
36
+ model_klass = find_model_klass(dionysus_event) or return
37
+ config.instrumenter.instrument("dionysus.consume.#{topic}.batch_number_#{batch_number}.persist.dionysus_destroy") do
38
+ model_klass.dionysus_destroy(dionysus_event.transformed_data)
39
+ end
40
+ end
41
+
42
+ def find_model_klass(dionysus_event)
43
+ config.model_factory.for_model(dionysus_event.model_name)
44
+ end
45
+
46
+ def persist_standard_event(dionysus_event, batch_number)
47
+ Array.wrap(dionysus_event.transformed_data).each do |deseralized_record|
48
+ model_klass = find_model_klass(dionysus_event) or return
49
+ attributes = deseralized_record.attributes
50
+ has_one_relationships = deseralized_record.has_one
51
+ has_many_relationships = deseralized_record.has_many
52
+ synced_id = deseralized_record.synced_id
53
+
54
+ if synced_id.nil?
55
+ Dionysus.logger.error("[Dionysus] synced_id nil for #{deseralized_record}, that should never happen!")
56
+ next
57
+ end
58
+
59
+ record = Dionysus::Consumer::SynchronizableModel.new(config,
60
+ model_klass.find_or_initialize_by(config.synced_id_attribute => synced_id))
61
+ event_updated_at = deseralized_record.synced_updated_at || deseralized_record.synced_created_at
62
+
63
+ next unless record.persist_with_dionysus?(event_updated_at)
64
+
65
+ record.assign_attributes_from_dionysus(attributes)
66
+ if dionysus_event.destroyed?
67
+ instrument("dionysus.consume.#{topic}.batch_number_#{batch_number}.persist.remove_with_dionysus") do
68
+ record.remove_with_dionysus(deseralized_record) if dionysus_event.aggregate_root?
69
+ end
70
+ else
71
+ instrument("dionysus.consume.#{topic}.batch_number_#{batch_number}.persist.restore_with_dionysus") do
72
+ record.restore_with_dionysus if record.restorable?(deseralized_record)
73
+ end
74
+ end
75
+
76
+ dionysus_event.local_changes[[dionysus_event.model_name, synced_id]] = record.changes if record.changes.present?
77
+
78
+ instrument("dionysus.consume.#{topic}.batch_number_#{batch_number}.persist.save") do
79
+ record.save unless record.destroyed?
80
+ end
81
+
82
+ instrument("dionysus.consume.#{topic}.batch_number_#{batch_number}.persist.persist_to_many_relationships") do
83
+ has_many_relationships.each do |relationship_name, relationship_records|
84
+ persist_to_many_relationship(dionysus_event, relationship_name, record, relationship_records,
85
+ batch_number)
86
+ end
87
+ end
88
+
89
+ instrument("dionysus.consume.#{topic}.batch_number_#{batch_number}.persist.persist_to_one_relationships") do
90
+ has_one_relationships.each do |relationship_name, relationship_record|
91
+ persist_to_one_relationship(dionysus_event, relationship_name, record, relationship_record,
92
+ batch_number)
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ def log_unknown_event_type(dionysus_event)
99
+ Dionysus.logger.debug("[Dionysus] unknown event type #{dionysus_event.event_name}")
100
+ end
101
+
102
+ def persist_to_one_relationship(original_event, relationship_name, parent_model_record, record, batch_number)
103
+ instrumentation_arguments = {
104
+ event_name: original_event.event_name,
105
+ parent_model_record: parent_model_record.model_name.to_s,
106
+ relationship_name: relationship_name
107
+ }
108
+ instrument(
109
+ "dionysus.consume.#{topic}.batch_number_#{batch_number}.persist.persist_to_one_relationship.#{relationship_name}", instrumentation_arguments
110
+ ) do
111
+ return if parent_model_record.nil? || record.nil?
112
+
113
+ records = Array.wrap(record)
114
+ dionysus_event = Dionysus::Consumer::DionysusEvent.new(original_event.event_name,
115
+ relationship_name, records, aggregate_root: false)
116
+
117
+ instrument("dionysus.consume.#{topic}.batch_number_#{batch_number}.persist.persist_to_one_relationship.#{relationship_name}.persist") do
118
+ persist(dionysus_event, batch_number)
119
+ original_event.local_changes.merge!(dionysus_event.local_changes)
120
+ end
121
+
122
+ instrument("dionysus.consume.#{topic}.batch_number_#{batch_number}.persist.persist_to_one_relationship.#{relationship_name}.resolve_to_one_association") do
123
+ parent_model_record.resolve_to_one_association(relationship_name, record.synced_id)
124
+ end
125
+ end
126
+ end
127
+
128
+ def persist_to_many_relationship(original_event, relationship_name, parent_model_record, records, batch_number)
129
+ instrumentation_arguments = {
130
+ event_name: original_event.event_name,
131
+ parent_model_record: parent_model_record.model_name.to_s,
132
+ relationship_name: relationship_name
133
+ }
134
+ instrument(
135
+ "dionysus.consume.#{topic}.batch_number_#{batch_number}.persist.persist_to_many_relationship.#{relationship_name}", instrumentation_arguments
136
+ ) do
137
+ return if parent_model_record.nil? || records.nil?
138
+
139
+ dionysus_event = Dionysus::Consumer::DionysusEvent.new(original_event.event_name,
140
+ relationship_name, records, aggregate_root: false)
141
+
142
+ instrument("dionysus.consume.#{topic}.batch_number_#{batch_number}.persist.persist_to_many_relationship.#{relationship_name}.persist") do
143
+ persist(dionysus_event, batch_number)
144
+ original_event.local_changes.merge!(dionysus_event.local_changes)
145
+ end
146
+
147
+ synced_ids_of_related_records = records.map(&:synced_id)
148
+ instrument("dionysus.consume.#{topic}.batch_number_#{batch_number}.persist.persist_to_many_relationship.#{relationship_name}.resolve_to_many_association") do
149
+ parent_model_record.resolve_to_many_association(relationship_name, synced_ids_of_related_records)
150
+ end
151
+ end
152
+ end
153
+
154
+ def instrument(label, options = {}, &block)
155
+ config.instrumenter.instrument(label, options, &block)
156
+ end
157
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dionysus::Consumer::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, :deserializer_klass, :consumers
23
+
24
+ def initialize(namespace)
25
+ @namespace = namespace
26
+ @topics = []
27
+ @deserializer_klass = nil
28
+ @consumers = []
29
+ end
30
+
31
+ def deserializer(deserializer_klass)
32
+ @deserializer_klass = deserializer_klass
33
+ end
34
+
35
+ def topic(name, options = {}, &block)
36
+ new_topic = Topic.new(namespace, name, deserializer_klass, options, &block)
37
+ consumer = Dionysus::Consumer::KarafkaConsumerGenerator.new.generate(
38
+ Dionysus::Consumer.configuration, new_topic
39
+ )
40
+ consumers << consumer
41
+ new_topic.consumer = consumer
42
+ topics << new_topic
43
+ end
44
+
45
+ class Topic
46
+ attr_reader :namespace, :name, :deserializer_klass, :options, :extensions_block
47
+
48
+ attr_accessor :consumer
49
+
50
+ def initialize(namespace, name, deserializer_klass, options = {}, &block)
51
+ @namespace = namespace
52
+ @name = name
53
+ @deserializer_klass = deserializer_klass
54
+ @options = options
55
+ @extensions_block = block
56
+ end
57
+
58
+ def to_s
59
+ Dionysus::TopicName.new(namespace, name).to_s
60
+ end
61
+
62
+ def sidekiq_worker
63
+ options.fetch(:worker, nil)
64
+ end
65
+
66
+ def sidekiq_backend?
67
+ options.fetch(:sidekiq, false)
68
+ end
69
+
70
+ def consumer_base_class
71
+ options.fetch(:consumer_base_class, nil)
72
+ end
73
+
74
+ def concurrency
75
+ options.fetch(:concurrency, nil)
76
+ end
77
+
78
+ def params_batch_transformation
79
+ options.fetch(:params_batch_transformation,
80
+ Dionysus::Consumer::ParamsBatchTransformations::RemoveDuplicatesStrategy.new)
81
+ end
82
+ end
83
+ end
84
+ end