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,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
|
data/lib/dionysus/rb.rb
ADDED
@@ -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,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,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
|