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.
- 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,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Dionysus::Consumer::SyncedData::AssignColumnsFromSyncedData
|
|
4
|
+
attr_reader :config
|
|
5
|
+
private :config
|
|
6
|
+
|
|
7
|
+
def initialize(config: Dionysus::Consumer.configuration)
|
|
8
|
+
@config = config
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(collection, columns)
|
|
12
|
+
collection.each { |record| record.update!(hash_of_attributes(record, columns)) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
delegate :resolve_synced_data_hash_proc, to: :config
|
|
18
|
+
|
|
19
|
+
def hash_of_attributes(record, columns)
|
|
20
|
+
columns
|
|
21
|
+
.to_h { |column| [column, fetch_value_from_synced_data(record, column)] }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fetch_value_from_synced_data(record, column)
|
|
25
|
+
resolve_synced_data_hash_proc.call(record).stringify_keys[column]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Dionysus::Consumer::SyncedData::AssignColumnsFromSyncedDataJob
|
|
4
|
+
include Sidekiq::Worker
|
|
5
|
+
|
|
6
|
+
sidekiq_options queue: Dionysus::Consumer::Config.default_sidekiq_queue
|
|
7
|
+
|
|
8
|
+
def self.enqueue(model_klass, columns, batch_size: 1000)
|
|
9
|
+
primary_key = model_klass.primary_key
|
|
10
|
+
model_klass.select(:id).find_in_batches(batch_size: batch_size).with_index do |records, index|
|
|
11
|
+
Dionysus.logger.info "[AssignColumnsFromSyncedDataJob] enqueue batch: #{index}"
|
|
12
|
+
model_name = model_klass.model_name.to_s
|
|
13
|
+
ids = records.map { |r| r.public_send(primary_key) }
|
|
14
|
+
|
|
15
|
+
set(queue: Dionysus::Consumer.configuration.sidekiq_queue)
|
|
16
|
+
.perform_async(model_name, ids, columns)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def perform(model_name, ids, columns)
|
|
21
|
+
model_klass = model_name.constantize
|
|
22
|
+
collection = model_klass.where(model_klass.primary_key => ids)
|
|
23
|
+
|
|
24
|
+
Dionysus::Consumer::SyncedData::AssignColumnsFromSyncedData.new.call(collection, columns)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Dionysus::Consumer::SynchronizableModel < SimpleDelegator
|
|
4
|
+
attr_reader :config
|
|
5
|
+
private :config
|
|
6
|
+
|
|
7
|
+
def initialize(config, model)
|
|
8
|
+
@config = config
|
|
9
|
+
super(model)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def model
|
|
13
|
+
__getobj__
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def synced_at
|
|
17
|
+
if respond_to?(synced_updated_at_timestamp_attribute)
|
|
18
|
+
public_send(synced_updated_at_timestamp_attribute)
|
|
19
|
+
else
|
|
20
|
+
public_send(synced_created_at_timestamp_attribute)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def persist_with_dionysus?(event_updated_at)
|
|
25
|
+
(synced_at && event_updated_at && event_updated_at >= synced_at) || synced_at.nil? || event_updated_at.nil?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def assign_attributes_from_dionysus(attributes)
|
|
29
|
+
public_send("#{synced_data_attribute}=", attributes)
|
|
30
|
+
reverse_mapping = config.attributes_mapping_for_model(model.model_name).to_a.map(&:reverse).to_h
|
|
31
|
+
|
|
32
|
+
assignable_attributes = extract_assignable_attributes(attributes)
|
|
33
|
+
.map { |key, value| apply_mapping(reverse_mapping, key, value) }
|
|
34
|
+
.select { |attribute, _| respond_to?("#{attribute}=") }
|
|
35
|
+
.to_h
|
|
36
|
+
|
|
37
|
+
assign_attributes(assignable_attributes)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def remove_with_dionysus(deseralized_record)
|
|
41
|
+
if soft_deleteable_but_cannot_be_soft_deleted_via_attribute_assignment?(deseralized_record)
|
|
42
|
+
public_send(soft_delete_strategy)
|
|
43
|
+
elsif (deseralized_record.has_synced_canceled_at? && !respond_to?("#{soft_deleted_at_timestamp_attribute}=")) ||
|
|
44
|
+
!deseralized_record.has_synced_canceled_at?
|
|
45
|
+
|
|
46
|
+
destroy
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def restore_with_dionysus
|
|
51
|
+
model.public_send("#{soft_deleted_at_timestamp_attribute}=", nil)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def restorable?(deseralized_record)
|
|
55
|
+
respond_to?("#{soft_deleted_at_timestamp_attribute}=") && !deseralized_record.has_synced_canceled_at?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
delegate :soft_delete_strategy, :synced_created_at_timestamp_attribute,
|
|
61
|
+
:synced_updated_at_timestamp_attribute, :soft_deleted_at_timestamp_attribute,
|
|
62
|
+
:synced_data_attribute, to: :config
|
|
63
|
+
|
|
64
|
+
def soft_deleteable_but_cannot_be_soft_deleted_via_attribute_assignment?(deseralized_record)
|
|
65
|
+
(respond_to?(soft_delete_strategy) && !deseralized_record.has_synced_canceled_at?) ||
|
|
66
|
+
(respond_to?(soft_delete_strategy) && !respond_to?("#{soft_deleted_at_timestamp_attribute}="))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def extract_assignable_attributes(attributes)
|
|
70
|
+
attributes.clone.tap do |hash|
|
|
71
|
+
if synced_created_at_timestamp_attribute.to_s != "synced_created_at"
|
|
72
|
+
hash[synced_created_at_timestamp_attribute] =
|
|
73
|
+
hash["synced_created_at"]
|
|
74
|
+
end
|
|
75
|
+
if synced_updated_at_timestamp_attribute.to_s != "synced_updated_at"
|
|
76
|
+
hash[synced_updated_at_timestamp_attribute] =
|
|
77
|
+
hash["synced_updated_at"]
|
|
78
|
+
end
|
|
79
|
+
if soft_deleted_at_timestamp_attribute.to_s != "synced_canceled_at"
|
|
80
|
+
hash[soft_deleted_at_timestamp_attribute] =
|
|
81
|
+
hash["synced_canceled_at"]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def apply_mapping(reverse_mapping, key, value)
|
|
87
|
+
if reverse_mapping.key?(key.to_sym)
|
|
88
|
+
[reverse_mapping[key.to_sym], value]
|
|
89
|
+
else
|
|
90
|
+
[key, value]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Dionysus::Consumer::WorkersGroup
|
|
4
|
+
attr_reader :workers
|
|
5
|
+
private :workers
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@workers = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def <<(worker)
|
|
12
|
+
workers << worker
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def work
|
|
16
|
+
workers.map(&:join)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Dionysus::Consumer
|
|
4
|
+
def self.configuration
|
|
5
|
+
@configuration ||= Dionysus::Consumer::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::Consumer::Registry.new
|
|
18
|
+
registry.instance_eval(&config)
|
|
19
|
+
|
|
20
|
+
Dionysus.inject_routing!(registry)
|
|
21
|
+
|
|
22
|
+
configure do |configuration|
|
|
23
|
+
configuration.registry = registry
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.reset!
|
|
28
|
+
return if registry.nil?
|
|
29
|
+
|
|
30
|
+
registry.registrations.values.flat_map(&:consumers).each do |consumer_class|
|
|
31
|
+
Dionysus.send(:remove_const, consumer_class.name.demodulize.to_sym) if consumer_class.name
|
|
32
|
+
end
|
|
33
|
+
@configuration = nil
|
|
34
|
+
Dionysus.inject_routing!(nil)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Dionysus::Monitor < Dry::Monitor::Notifications
|
|
4
|
+
EVENTS = %w[
|
|
5
|
+
outbox_producer.started
|
|
6
|
+
outbox_producer.stopped
|
|
7
|
+
outbox_producer.shutting_down
|
|
8
|
+
outbox_producer.error
|
|
9
|
+
outbox_producer.publishing_failed
|
|
10
|
+
outbox_producer.published
|
|
11
|
+
outbox_producer.processing_topic
|
|
12
|
+
outbox_producer.processed_topic
|
|
13
|
+
outbox_producer.lock_exists_for_topic
|
|
14
|
+
outbox_producer.heartbeat
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
private_constant :EVENTS
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
super(:dionysus)
|
|
21
|
+
EVENTS.each { |event| register_event(event) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def subscribe(event)
|
|
25
|
+
return super if events.include?(event.to_s)
|
|
26
|
+
|
|
27
|
+
raise UnknownEventError.new(events, event)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def events
|
|
31
|
+
EVENTS
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class UnknownEventError < StandardError
|
|
35
|
+
attr_reader :available_events, :current_event
|
|
36
|
+
private :available_events, :current_event
|
|
37
|
+
|
|
38
|
+
def initialize(available_events, current_event)
|
|
39
|
+
super()
|
|
40
|
+
@available_events = available_events
|
|
41
|
+
@current_event = current_event
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def message
|
|
45
|
+
"unknown event: #{current_event}, the available events are: #{available_events.join(", ")}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Based on the responder concept from Karafka 1.4
|
|
4
|
+
class Dionysus::Producer::BaseResponder
|
|
5
|
+
class << self
|
|
6
|
+
attr_accessor :topics
|
|
7
|
+
|
|
8
|
+
def topic(topic_name)
|
|
9
|
+
self.topics ||= {}
|
|
10
|
+
self.topics[topic_name] = topic_name.to_s
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(*data)
|
|
14
|
+
new.call(*data)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :messages_buffer
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@messages_buffer = Hash.new { |h, k| h[k] = [] }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(*data)
|
|
25
|
+
respond(*data)
|
|
26
|
+
deliver
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def deliver
|
|
32
|
+
messages_buffer.each_value do |data_elements|
|
|
33
|
+
data_elements.each do |data, options|
|
|
34
|
+
Karafka.producer.produce_sync(payload: data, **options)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def respond(*_data)
|
|
40
|
+
raise "implement me"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def respond_to(topic, data, options = {})
|
|
44
|
+
messages_buffer[topic] << [data.to_json, options.merge(topic: topic.to_s)]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Dionysus::Producer::Config
|
|
4
|
+
attr_accessor :registry, :outbox_model, :database_connection_provider, :transaction_provider,
|
|
5
|
+
:datadog_statsd_client
|
|
6
|
+
|
|
7
|
+
attr_writer :instrumenter, :event_bus, :soft_delete_column, :default_partition_key, :outbox_worker_sleep_seconds,
|
|
8
|
+
:lock_client, :lock_expiry_time, :error_handler, :outbox_publishing_batch_size,
|
|
9
|
+
:transactional_outbox_enabled, :sidekiq_queue, :publisher_service_name,
|
|
10
|
+
:genesis_consistency_safety_delay, :hermes_event_producer, :publish_after_commit, :outbox_worker_publishing_delay,
|
|
11
|
+
:high_priority_sidekiq_queue, :observers_inline_maximum_size, :remove_consecutive_duplicates_before_publishing
|
|
12
|
+
|
|
13
|
+
def self.default_sidekiq_queue
|
|
14
|
+
:dionysus
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.high_priority_sidekiq_queue
|
|
18
|
+
:dionysus_high_priority
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def instrumenter
|
|
22
|
+
@instrumenter || Dionysus::Utils::NullInstrumenter
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def event_bus
|
|
26
|
+
@event_bus || Dionysus::Utils::NullEventBus
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def soft_delete_column
|
|
30
|
+
@soft_delete_column || "canceled_at"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def default_partition_key
|
|
34
|
+
@default_partition_key || :account_id
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def outbox_worker_sleep_seconds
|
|
38
|
+
return BigDecimal("0.2") if @outbox_worker_sleep_seconds.nil?
|
|
39
|
+
|
|
40
|
+
@outbox_worker_sleep_seconds.to_d
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def lock_client
|
|
44
|
+
@lock_client || Dionysus::Utils::NullLockClient
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def lock_expiry_time
|
|
48
|
+
@lock_expiry_time || 10_000
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def error_handler
|
|
52
|
+
@error_handler || Dionysus::Utils::NullErrorHandler
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def outbox_publishing_batch_size
|
|
56
|
+
@outbox_publishing_batch_size || 100
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def transactional_outbox_enabled
|
|
60
|
+
return @transactional_outbox_enabled if defined?(@transactional_outbox_enabled)
|
|
61
|
+
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def sidekiq_queue
|
|
66
|
+
@sidekiq_queue || self.class.default_sidekiq_queue
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def publisher_service_name
|
|
70
|
+
@publisher_service_name || Karafka.producer.id
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def genesis_consistency_safety_delay
|
|
74
|
+
@genesis_consistency_safety_delay || 60.seconds
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def hermes_event_producer
|
|
78
|
+
@hermes_event_producer || Dionysus::Utils::NullHermesEventProducer
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def publish_after_commit
|
|
82
|
+
return @publish_after_commit if defined?(@publish_after_commit)
|
|
83
|
+
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def outbox_worker_publishing_delay
|
|
88
|
+
(@outbox_worker_publishing_delay || 0).to_d.seconds
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def high_priority_sidekiq_queue
|
|
92
|
+
@high_priority_sidekiq_queue || self.class.high_priority_sidekiq_queue
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def observers_inline_maximum_size
|
|
96
|
+
@observers_inline_maximum_size || 1000
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def remove_consecutive_duplicates_before_publishing
|
|
100
|
+
return @remove_consecutive_duplicates_before_publishing if defined?(@remove_consecutive_duplicates_before_publishing)
|
|
101
|
+
|
|
102
|
+
false
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Dionysus::Producer::DeletedRecordSerializer < Dionysus::Producer::ModelSerializer
|
|
4
|
+
def as_json
|
|
5
|
+
super.merge(primary_key => primary_key_value)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def primary_key
|
|
11
|
+
record.class.primary_key
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def primary_key_value
|
|
15
|
+
record.public_send(primary_key)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
if defined?(Hermes)
|
|
4
|
+
class Dionysus::Producer::Genesis::Performed < Hermes::BaseEvent
|
|
5
|
+
attribute :model, Dry.Types::Strict::String
|
|
6
|
+
attribute :service, Dry.Types::Strict::String
|
|
7
|
+
attribute :topic, Dry.Types::Strict::String
|
|
8
|
+
attribute :start_at, Dry.Types::Nominal::DateTime
|
|
9
|
+
attribute :end_at, Dry.Types::Nominal::DateTime
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Dionysus::Producer::Genesis::StreamJob
|
|
4
|
+
include Sidekiq::Worker
|
|
5
|
+
|
|
6
|
+
sidekiq_options queue: Dionysus::Producer::Config.default_sidekiq_queue
|
|
7
|
+
|
|
8
|
+
def perform(topic, model_klass, from, to, number_of_days, streamer_job)
|
|
9
|
+
Dionysus::Producer::Genesis::Streamer
|
|
10
|
+
.new(job_class: streamer_job.constantize)
|
|
11
|
+
.stream(topic, model_klass.constantize, from, to, number_of_days: number_of_days)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Dionysus::Producer::Genesis::Streamer::BaseJob
|
|
4
|
+
include Sidekiq::Worker
|
|
5
|
+
|
|
6
|
+
sidekiq_options queue: Dionysus::Producer::Config.default_sidekiq_queue
|
|
7
|
+
|
|
8
|
+
ONE_DAY_IN_SECONDS = 60 * 60 * 24
|
|
9
|
+
BATCH_SIZE = 1000
|
|
10
|
+
|
|
11
|
+
def self.enqueue(relation, model_class, topic, number_of_days: 1, batch_size: BATCH_SIZE)
|
|
12
|
+
distributor = Dionysus::Utils::SidekiqBatchedJobDistributor.new(
|
|
13
|
+
batch_size: batch_size,
|
|
14
|
+
units_count: relation.count,
|
|
15
|
+
time_range_in_seconds: (ONE_DAY_IN_SECONDS * number_of_days)
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
relation.in_batches(of: batch_size).lazy.each_with_index do |batch_relation, batch_number|
|
|
19
|
+
distributor.enqueue_batch(
|
|
20
|
+
self,
|
|
21
|
+
Dionysus::Producer.configuration.sidekiq_queue,
|
|
22
|
+
batch_number,
|
|
23
|
+
batch_relation.pluck(model_class.primary_key).sort,
|
|
24
|
+
model_class.to_s,
|
|
25
|
+
topic
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def perform(ids, resource_name, topic)
|
|
31
|
+
model_class = resource_name.constantize
|
|
32
|
+
primary_key_column = model_class.primary_key
|
|
33
|
+
|
|
34
|
+
model_class
|
|
35
|
+
.where(primary_key_column => ids)
|
|
36
|
+
.find_each { |entity| call(entity, topic) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def call(_item, _topic)
|
|
42
|
+
raise "implement me!"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Dionysus::Producer::Genesis::Streamer::StandardJob < Dionysus::Producer::Genesis::Streamer::BaseJob
|
|
4
|
+
DESTROYED_EVENT_TYPE = "destroyed"
|
|
5
|
+
UPDATED_EVENT_TYPE = "updated"
|
|
6
|
+
GENESIS_TOPIC_SUFFIX = "genesis"
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
delegate :configuration, :outbox_model, :outbox_publisher, to: Dionysus::Producer
|
|
11
|
+
delegate :outbox_model, to: :configuration
|
|
12
|
+
|
|
13
|
+
def call(item, topic)
|
|
14
|
+
publishable = Dionysus::Producer::Outbox::Publishable.new(item)
|
|
15
|
+
outbox_record = Dionysus::Producer.configuration.outbox_model.new(
|
|
16
|
+
resource_class: publishable.model_name.to_s,
|
|
17
|
+
resource_id: publishable.publishable_id,
|
|
18
|
+
event_name: event_name_for(publishable),
|
|
19
|
+
topic: topic
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
options = {}
|
|
23
|
+
options[:genesis_only] = true if genesis_only?(topic)
|
|
24
|
+
|
|
25
|
+
outbox_publisher.publish(outbox_record, options)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def event_name_for(publishable)
|
|
29
|
+
Dionysus::Producer::Outbox::EventName
|
|
30
|
+
.new(publishable.resource_name)
|
|
31
|
+
.for_event_type(event_type(publishable))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def event_type(publishable)
|
|
35
|
+
return DESTROYED_EVENT_TYPE if publishable.soft_deletable? && publishable.soft_deleted?
|
|
36
|
+
|
|
37
|
+
UPDATED_EVENT_TYPE
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def genesis_only?(topic)
|
|
41
|
+
topic.ends_with?(GENESIS_TOPIC_SUFFIX)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Dionysus::Producer::Genesis::Streamer
|
|
4
|
+
attr_reader :job_class, :config
|
|
5
|
+
private :job_class, :config
|
|
6
|
+
|
|
7
|
+
def initialize(job_class: Dionysus::Producer::Genesis::Streamer::StandardJob,
|
|
8
|
+
config: Dionysus::Producer.configuration)
|
|
9
|
+
@job_class = job_class
|
|
10
|
+
@config = config
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def stream(topic, model_class, from, to, options = {})
|
|
14
|
+
resources = fetch_resources(model_class, from, to, options)
|
|
15
|
+
job_class.enqueue(
|
|
16
|
+
resources,
|
|
17
|
+
model_class,
|
|
18
|
+
topic,
|
|
19
|
+
number_of_days: options.fetch(:number_of_days, 1),
|
|
20
|
+
batch_size: options.fetch(:batch_size, 1000)
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
delegate :soft_delete_column, to: :config
|
|
27
|
+
|
|
28
|
+
def fetch_resources(resource_class, from, to, options_hash)
|
|
29
|
+
records = resource_class
|
|
30
|
+
records = resource_class.where("updated_at BETWEEN ? AND ?", from, to) if from.present? && to.present?
|
|
31
|
+
if visible_only?(options_hash) && records.column_names.include?(soft_delete_column.to_s)
|
|
32
|
+
records = records.where(soft_delete_column => nil)
|
|
33
|
+
end
|
|
34
|
+
records
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def visible_only?(options_hash)
|
|
38
|
+
options_hash.fetch(:visible_only, false)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Dionysus::Producer::Genesis
|
|
4
|
+
attr_reader :config
|
|
5
|
+
private :config
|
|
6
|
+
|
|
7
|
+
def initialize(config: Dionysus::Producer.configuration)
|
|
8
|
+
@config = config
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def stream(topic:, model:, number_of_days:, from: nil, to: nil,
|
|
12
|
+
streamer_job: Dionysus::Producer::Genesis::Streamer::StandardJob)
|
|
13
|
+
ensure_genesis_not_executed_for_model_that_is_only_a_dependency(model, topic)
|
|
14
|
+
enqueue_stream_model_for_topic(topic, model, from, to, number_of_days, streamer_job)
|
|
15
|
+
publish_genesis_performed(model: model, topic: topic, number_of_days: number_of_days)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
delegate :publisher_service_name, :genesis_consistency_safety_delay, :hermes_event_producer, :sidekiq_queue,
|
|
21
|
+
to: :config
|
|
22
|
+
delegate :responders_for_model_for_topic, :responders_for_dependency_parent_for_topic,
|
|
23
|
+
to: Dionysus::Producer
|
|
24
|
+
|
|
25
|
+
def ensure_genesis_not_executed_for_model_that_is_only_a_dependency(model, topic)
|
|
26
|
+
if responders_for_model_for_topic(model,
|
|
27
|
+
topic).empty? && responders_for_dependency_parent_for_topic(model, topic).any?
|
|
28
|
+
raise CannotExecuteGenesisForModelThatIsOnlyDependency.new(model, topic)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def enqueue_stream_model_for_topic(topic, model, from, to, number_of_days, streamer_job)
|
|
33
|
+
Dionysus::Producer::Genesis::StreamJob
|
|
34
|
+
.set(queue: sidekiq_queue)
|
|
35
|
+
.perform_async(topic.to_s, model.to_s, from.as_json, to.as_json, number_of_days.to_i, streamer_job.to_s)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def publish_genesis_performed(model:, topic:, number_of_days:)
|
|
39
|
+
event = Dionysus::Producer::Genesis::Performed.new(
|
|
40
|
+
model: model.to_s,
|
|
41
|
+
service: publisher_service_name,
|
|
42
|
+
topic: topic,
|
|
43
|
+
start_at: Time.current,
|
|
44
|
+
end_at: Time.current + number_of_days.days + genesis_consistency_safety_delay
|
|
45
|
+
)
|
|
46
|
+
hermes_event_producer.publish(event)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class CannotExecuteGenesisForModelThatIsOnlyDependency < StandardError
|
|
50
|
+
attr_reader :model, :topic
|
|
51
|
+
private :model, :topic
|
|
52
|
+
def initialize(model, topic)
|
|
53
|
+
super()
|
|
54
|
+
@model = model
|
|
55
|
+
@topic = topic
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def message
|
|
59
|
+
"Cannot execute genesis for model #{model}, #{topic} because that is only a dependency. Execute it for parent instead"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|