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,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dionysus::Producer::Outbox::Model
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
OBSERVER_TOPIC = "__outbox_observer__"
|
7
|
+
CHANGESET_COLUMN = "changeset"
|
8
|
+
private_constant :OBSERVER_TOPIC, :CHANGESET_COLUMN
|
9
|
+
|
10
|
+
def self.observer_topic
|
11
|
+
OBSERVER_TOPIC
|
12
|
+
end
|
13
|
+
|
14
|
+
included do
|
15
|
+
scope :fetch_publishable, lambda { |batch_size, topic|
|
16
|
+
outbox_worker_publishing_delay = Dionysus::Producer.configuration.outbox_worker_publishing_delay
|
17
|
+
|
18
|
+
records = where(published_at: nil, topic: topic)
|
19
|
+
.where("retry_at IS NULL OR retry_at <= ?", Time.current)
|
20
|
+
.order(created_at: :asc)
|
21
|
+
.limit(batch_size)
|
22
|
+
if outbox_worker_publishing_delay > 0.seconds
|
23
|
+
records = records.where("created_at <= ?", Time.current + outbox_worker_publishing_delay)
|
24
|
+
end
|
25
|
+
records
|
26
|
+
}
|
27
|
+
scope :published_since, ->(time) { where("published_at >= ?", time) }
|
28
|
+
scope :not_published, -> { where(published_at: nil) }
|
29
|
+
|
30
|
+
belongs_to :resource, polymorphic: true, foreign_type: :resource_class, optional: true
|
31
|
+
|
32
|
+
def self.pending_topics
|
33
|
+
not_published.select("DISTINCT topic").map(&:topic)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.handles_changeset?
|
37
|
+
column_names.include?(CHANGESET_COLUMN)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.encrypts_changeset!
|
41
|
+
define_method :changeset= do |payload|
|
42
|
+
super(payload.to_json)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def observer?
|
48
|
+
topic == Dionysus::Producer::Outbox::Model.observer_topic
|
49
|
+
end
|
50
|
+
|
51
|
+
def transformed_changeset
|
52
|
+
return {} unless self.class.handles_changeset?
|
53
|
+
|
54
|
+
if changeset.respond_to?(:to_hash)
|
55
|
+
changeset.symbolize_keys
|
56
|
+
else
|
57
|
+
JSON.parse(changeset).symbolize_keys
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def published?
|
62
|
+
published_at.present?
|
63
|
+
end
|
64
|
+
|
65
|
+
def failed?
|
66
|
+
failed_at.present?
|
67
|
+
end
|
68
|
+
|
69
|
+
def handle_error(raised_error, clock: Time)
|
70
|
+
@error = raised_error
|
71
|
+
self.error_class = raised_error.class
|
72
|
+
self.error_message = raised_error.message
|
73
|
+
self.failed_at = clock.current
|
74
|
+
self.attempts ||= 0
|
75
|
+
self.attempts += 1
|
76
|
+
self.retry_at = clock.current.advance(seconds: Dionysus::Utils::ExponentialBackoff.backoff_for(5,
|
77
|
+
attempts))
|
78
|
+
end
|
79
|
+
|
80
|
+
def error
|
81
|
+
if error_class_arity == 1 || error_class_arity == -1
|
82
|
+
error_class.constantize.new(error_message)
|
83
|
+
else
|
84
|
+
StandardError.new("#{error_class_constant}: #{error_message}")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def resource_created_at
|
89
|
+
return created_at if resource.nil?
|
90
|
+
|
91
|
+
resource.created_at
|
92
|
+
end
|
93
|
+
|
94
|
+
def publishing_latency
|
95
|
+
return unless published?
|
96
|
+
|
97
|
+
published_at - created_at
|
98
|
+
end
|
99
|
+
|
100
|
+
def created_event?
|
101
|
+
event_name.to_s.end_with?("created")
|
102
|
+
end
|
103
|
+
|
104
|
+
def updated_event?
|
105
|
+
event_name.to_s.end_with?("updated")
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def error_class_constant
|
111
|
+
error_class.constantize
|
112
|
+
end
|
113
|
+
|
114
|
+
def error_class_arity
|
115
|
+
error_class_constant.instance_method(:initialize).arity
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Dionysus::Producer::Outbox::Producer
|
4
|
+
attr_reader :config
|
5
|
+
private :config
|
6
|
+
|
7
|
+
def initialize(config: Dionysus::Producer.configuration)
|
8
|
+
@config = config
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(topic, batch_size: config.outbox_publishing_batch_size)
|
12
|
+
outbox_model.fetch_publishable(batch_size, topic).to_a.tap do |records_to_publish|
|
13
|
+
records_processor.call(records_to_publish) do |record|
|
14
|
+
yield record if block_given?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
delegate :outbox_model, to: :config
|
22
|
+
|
23
|
+
def records_processor
|
24
|
+
@records_processor ||= Dionysus::Producer::Outbox::RecordsProcessor.new
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Dionysus::Producer::Outbox::Publishable < SimpleDelegator
|
4
|
+
attr_reader :model, :config, :soft_delete_column
|
5
|
+
private :config, :soft_delete_column
|
6
|
+
|
7
|
+
def initialize(model, config: Dionysus::Producer.configuration)
|
8
|
+
@model = model
|
9
|
+
@config = config
|
10
|
+
@soft_delete_column = config.soft_delete_column.to_s
|
11
|
+
super(model)
|
12
|
+
end
|
13
|
+
|
14
|
+
def model_class
|
15
|
+
model.class
|
16
|
+
end
|
17
|
+
|
18
|
+
def primary_key_attribute
|
19
|
+
model.class.primary_key
|
20
|
+
end
|
21
|
+
|
22
|
+
def publishable_id
|
23
|
+
model.public_send(primary_key_attribute)
|
24
|
+
end
|
25
|
+
|
26
|
+
def model_name
|
27
|
+
model.class.model_name
|
28
|
+
end
|
29
|
+
|
30
|
+
def resource_name
|
31
|
+
model_name.singular
|
32
|
+
end
|
33
|
+
|
34
|
+
def previously_changed?
|
35
|
+
previous_changes.present?
|
36
|
+
end
|
37
|
+
|
38
|
+
def previous_changes_include_canceled?
|
39
|
+
!!previous_changes[soft_delete_column]
|
40
|
+
end
|
41
|
+
|
42
|
+
def previous_changes_uncanceled?
|
43
|
+
previous_changes_include_canceled? && previous_changes[soft_delete_column][0].present? && visible?
|
44
|
+
end
|
45
|
+
|
46
|
+
def previous_changes_canceled?
|
47
|
+
previous_changes_include_canceled? && previous_changes[soft_delete_column][0].blank? && soft_deleted?
|
48
|
+
end
|
49
|
+
|
50
|
+
def previous_changes_still_canceled?
|
51
|
+
previous_changes_include_canceled? && previous_changes[soft_delete_column][0].present? && soft_deleted?
|
52
|
+
end
|
53
|
+
|
54
|
+
# TODO: Check if this is needed
|
55
|
+
def previous_changed_still_visible?
|
56
|
+
previous_changes_include_canceled? && previous_changes[soft_delete_column][0].blank? && visible?
|
57
|
+
end
|
58
|
+
|
59
|
+
def soft_deleted?
|
60
|
+
public_send(soft_delete_column).present?
|
61
|
+
end
|
62
|
+
|
63
|
+
def soft_deletable?
|
64
|
+
model.respond_to?(soft_delete_column)
|
65
|
+
end
|
66
|
+
|
67
|
+
def visible?
|
68
|
+
!soft_deleted?
|
69
|
+
end
|
70
|
+
|
71
|
+
def topics
|
72
|
+
top_level_topics = Dionysus::Producer
|
73
|
+
.responders_for(model.class)
|
74
|
+
.map(&:primary_topic)
|
75
|
+
topics_from_dependencies = Dionysus::Producer
|
76
|
+
.responders_for_dependency_parent(model.class)
|
77
|
+
.map(&:last)
|
78
|
+
.map(&:primary_topic)
|
79
|
+
|
80
|
+
[top_level_topics, topics_from_dependencies]
|
81
|
+
.flatten
|
82
|
+
.uniq
|
83
|
+
.tap { |standard_topics| standard_topics << observer_topic if add_observer_topic? }
|
84
|
+
end
|
85
|
+
|
86
|
+
def changeset
|
87
|
+
if model.destroyed?
|
88
|
+
{
|
89
|
+
model.class.primary_key => [model.public_send(model.class.primary_key), nil],
|
90
|
+
"created_at" => [model.created_at, nil]
|
91
|
+
}
|
92
|
+
else
|
93
|
+
model.previous_changes
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
delegate :observer_topic, to: Dionysus::Producer::Outbox::Model
|
100
|
+
delegate :observers_with_responders_for, to: Dionysus::Producer
|
101
|
+
delegate :outbox_model, to: :config
|
102
|
+
|
103
|
+
def add_observer_topic?
|
104
|
+
outbox_model.handles_changeset? && observers_with_responders_for(self, changeset).any?
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Dionysus::Producer::Outbox::Publisher
|
4
|
+
attr_reader :config
|
5
|
+
private :config
|
6
|
+
|
7
|
+
def initialize(config:)
|
8
|
+
@config = config
|
9
|
+
end
|
10
|
+
|
11
|
+
def publish(outbox_record, options = {})
|
12
|
+
return if Dionysus::Producer::Suppressor.suppressed?
|
13
|
+
|
14
|
+
instrument("publishing_with_dionysus") do
|
15
|
+
resource_class = outbox_record.resource_class.constantize
|
16
|
+
primary_key = resource_class.primary_key
|
17
|
+
primary_key_value = outbox_record.resource_id
|
18
|
+
topic = outbox_record.topic
|
19
|
+
resource = resource_class.find_by(primary_key => primary_key_value) ||
|
20
|
+
resource_class.new(primary_key => primary_key_value)
|
21
|
+
event_name = outbox_record.event_name
|
22
|
+
if resource.new_record? && outbox_record.created_event?
|
23
|
+
logger.error(
|
24
|
+
"Attempted to publish #{resource.class}, id: #{resource.id} but it was deleted, that should never happen!"
|
25
|
+
)
|
26
|
+
return
|
27
|
+
end
|
28
|
+
|
29
|
+
if resource.new_record? && outbox_record.updated_event?
|
30
|
+
logger.error(
|
31
|
+
"There was an update of #{resource.class}, id: #{resource.id} but it was deleted, that should never happen!"
|
32
|
+
)
|
33
|
+
return
|
34
|
+
end
|
35
|
+
|
36
|
+
publish_for_top_level_resource(outbox_record, resource, event_name, topic, options)
|
37
|
+
publish_for_dependency(resource, topic, options)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def publish_observers(outbox_record)
|
42
|
+
return if Dionysus::Producer::Suppressor.suppressed?
|
43
|
+
|
44
|
+
instrument("publishing_observers_with_dionysus") do
|
45
|
+
resource_class = outbox_record.resource_class.constantize
|
46
|
+
primary_key = resource_class.primary_key
|
47
|
+
primary_key_value = outbox_record.resource_id
|
48
|
+
resource = resource_class.find_by(primary_key => primary_key_value) ||
|
49
|
+
resource_class.new(primary_key => primary_key_value)
|
50
|
+
changeset = outbox_record.transformed_changeset
|
51
|
+
|
52
|
+
Dionysus::Producer.observers_with_responders_for(resource,
|
53
|
+
changeset).each do |observers, responder|
|
54
|
+
if observers.count > config.observers_inline_maximum_size
|
55
|
+
execute_genesis_for_observers(observers, responder)
|
56
|
+
else
|
57
|
+
observers.each { |observer_record| publish_observer(observer_record, responder) }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
delegate :instrumenter, :error_handler, to: :config
|
66
|
+
delegate :instrument, to: :instrumenter
|
67
|
+
delegate :logger, to: Dionysus
|
68
|
+
|
69
|
+
def publish_for_top_level_resource(outbox_record, resource, event_name, topic, options)
|
70
|
+
Dionysus::Producer.responders_for_model_for_topic(resource.class, topic).each do |responder|
|
71
|
+
partition_key = outbox_record.partition_key.presence || Dionysus::Producer::PartitionKey.new(
|
72
|
+
resource
|
73
|
+
).to_key(responder: responder)
|
74
|
+
key = Dionysus::Producer::Key.new(resource).to_key
|
75
|
+
|
76
|
+
responder.call(generate_message(event_name, resource), options.merge(partition_key: partition_key, key: key))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def publish_for_dependency(resource, topic, options)
|
81
|
+
Dionysus::Producer.responders_for_dependency_parent_for_topic(resource.class,
|
82
|
+
topic).each do |parent_klass, responder|
|
83
|
+
parent_event_name = Dionysus::Producer::Outbox::EventName.new(
|
84
|
+
parent_klass.model_name.singular
|
85
|
+
).updated
|
86
|
+
|
87
|
+
if resource.class.reflect_on_association(parent_klass.model_name.singular)
|
88
|
+
parent_records = [resource.public_send(parent_klass.model_name.singular)]
|
89
|
+
elsif resource.class.reflect_on_association(parent_klass.model_name.plural)
|
90
|
+
parent_records = resource.public_send(parent_klass.model_name.plural)
|
91
|
+
else
|
92
|
+
next
|
93
|
+
end
|
94
|
+
|
95
|
+
example_parent_record = parent_records.first or next
|
96
|
+
partition_key = Dionysus::Producer::PartitionKey.new(example_parent_record, config: config)
|
97
|
+
.to_key(responder: responder)
|
98
|
+
key = Dionysus::Producer::Key.new(example_parent_record).to_key
|
99
|
+
|
100
|
+
parent_records.each do |parent_record|
|
101
|
+
responder.call(generate_message(parent_event_name, parent_record),
|
102
|
+
options.merge(partition_key: partition_key, key: key))
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def publish_observer(observer_record, responder)
|
108
|
+
publishable = Dionysus::Producer::Outbox::Publishable.new(observer_record)
|
109
|
+
partition_key = Dionysus::Producer::PartitionKey.new(publishable).to_key(responder: responder)
|
110
|
+
key = Dionysus::Producer::Key.new(publishable).to_key
|
111
|
+
event_name = Dionysus::Producer::Outbox::EventName.new(publishable.resource_name).updated
|
112
|
+
|
113
|
+
responder.call(generate_message(event_name, publishable), partition_key: partition_key, key: key)
|
114
|
+
end
|
115
|
+
|
116
|
+
def generate_message(event_name, resource)
|
117
|
+
[[event_name, resource, {}]]
|
118
|
+
end
|
119
|
+
|
120
|
+
def execute_genesis_for_observers(observers, responder)
|
121
|
+
resource_class = observers.first.class
|
122
|
+
primary_key = resource_class.primary_key
|
123
|
+
|
124
|
+
Dionysus::Producer::Genesis::Streamer::StandardJob.enqueue(
|
125
|
+
resource_class.where(primary_key => observers),
|
126
|
+
resource_class,
|
127
|
+
responder.primary_topic,
|
128
|
+
number_of_days: 0.1
|
129
|
+
)
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Dionysus::Producer::Outbox::RecordsProcessor
|
4
|
+
attr_reader :config
|
5
|
+
private :config
|
6
|
+
|
7
|
+
def initialize(config: Dionysus::Producer.configuration)
|
8
|
+
@config = config
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(records)
|
12
|
+
failed_records = []
|
13
|
+
records_to_publish = resolve_records_to_publish(records)
|
14
|
+
records_to_publish.each do |record|
|
15
|
+
begin
|
16
|
+
publish(record)
|
17
|
+
rescue => e
|
18
|
+
record.handle_error(e)
|
19
|
+
record.save!
|
20
|
+
failed_records << record
|
21
|
+
end
|
22
|
+
yield record if block_given?
|
23
|
+
end
|
24
|
+
published_records = records - failed_records
|
25
|
+
mark_as_published(published_records)
|
26
|
+
records
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
delegate :outbox_model, to: :config
|
32
|
+
|
33
|
+
def resolve_records_to_publish(records)
|
34
|
+
return records unless config.remove_consecutive_duplicates_before_publishing
|
35
|
+
|
36
|
+
Dionysus::Producer::Outbox::DuplicatesFilter.call(records)
|
37
|
+
end
|
38
|
+
|
39
|
+
def publish(record)
|
40
|
+
if record.observer?
|
41
|
+
outbox_publisher.publish_observers(record)
|
42
|
+
else
|
43
|
+
outbox_publisher.publish(record)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def outbox_publisher
|
48
|
+
@outbox_publisher ||= Dionysus::Producer.outbox_publisher
|
49
|
+
end
|
50
|
+
|
51
|
+
def mark_as_published(published_records)
|
52
|
+
outbox_model
|
53
|
+
.where(id: published_records)
|
54
|
+
.update_all(published_at: Time.current, error_class: nil, error_message: nil, failed_at: nil, retry_at: nil)
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Dionysus::Producer::Outbox::Runner
|
4
|
+
attr_reader :logger, :id, :config
|
5
|
+
private :logger, :config
|
6
|
+
|
7
|
+
def initialize(config: Dionysus::Producer.configuration,
|
8
|
+
logger: Dionysus.logger)
|
9
|
+
@id = SecureRandom.uuid
|
10
|
+
@logger = logger
|
11
|
+
logger.push_tags("Dionysus::Producer::Outbox::Runners #{id}") if logger.respond_to?(:push_tags)
|
12
|
+
@config = config
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
log("started")
|
17
|
+
instrument("outbox_producer.started")
|
18
|
+
@should_stop = false
|
19
|
+
ensure_database_connection!
|
20
|
+
loop do
|
21
|
+
if @should_stop
|
22
|
+
instrument("outbox_producer.shutting_down")
|
23
|
+
log("shutting down")
|
24
|
+
break
|
25
|
+
end
|
26
|
+
process_topics
|
27
|
+
instrument("outbox_producer.heartbeat")
|
28
|
+
sleep outbox_worker_sleep_seconds
|
29
|
+
end
|
30
|
+
rescue => e
|
31
|
+
error_handler.capture_exception(e)
|
32
|
+
log("error: #{e} #{e.message}")
|
33
|
+
instrument("outbox_producer.error", error: e, error_message: e.message)
|
34
|
+
raise e
|
35
|
+
end
|
36
|
+
|
37
|
+
def stop
|
38
|
+
log("Outbox worker stopping")
|
39
|
+
instrument("outbox_producer.stopped")
|
40
|
+
@should_stop = true
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
delegate :outbox_worker_sleep_seconds, :database_connection_provider, :outbox_model,
|
46
|
+
:lock_client, :lock_expiry_time, :error_handler,
|
47
|
+
:outbox_publishing_batch_size, to: :config
|
48
|
+
|
49
|
+
delegate :pending_topics, to: :outbox_model
|
50
|
+
delegate :lock, to: :lock_client
|
51
|
+
|
52
|
+
def process_topics
|
53
|
+
pending_topics.each do |topic|
|
54
|
+
instrument("outbox_producer.processing_topic", topic: topic) do
|
55
|
+
tracer.trace("outbox_producer", topic) do
|
56
|
+
lock(lock_name(topic), lock_expiry_time) do |locked|
|
57
|
+
if locked
|
58
|
+
producer.call(topic, batch_size: outbox_publishing_batch_size) do |record|
|
59
|
+
if record.failed?
|
60
|
+
instrument("outbox_producer.publishing_failed", outbox_record: record)
|
61
|
+
error("failed to publish #{record.inspect}")
|
62
|
+
error_handler.capture_exception(record.error)
|
63
|
+
else
|
64
|
+
debug("published #{record.inspect}")
|
65
|
+
instrument("outbox_producer.published", outbox_record: record)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
instrument("outbox_producer.processed_topic", topic: topic)
|
69
|
+
else
|
70
|
+
debug("lock exists for #{topic} topic")
|
71
|
+
instrument("outbox_producer.lock_exists_for_topic", topic: topic)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def ensure_database_connection!
|
80
|
+
database_connection_provider.connection.reconnect!
|
81
|
+
end
|
82
|
+
|
83
|
+
def log(message)
|
84
|
+
logger.info("#{log_prefix} #{message}")
|
85
|
+
end
|
86
|
+
|
87
|
+
def debug(message)
|
88
|
+
logger.debug("#{log_prefix} #{message}")
|
89
|
+
end
|
90
|
+
|
91
|
+
def error(message)
|
92
|
+
logger.error("#{log_prefix} #{message}")
|
93
|
+
end
|
94
|
+
|
95
|
+
def lock_name(topic)
|
96
|
+
"dionysus_#{topic}_lock"
|
97
|
+
end
|
98
|
+
|
99
|
+
def log_prefix
|
100
|
+
"[Dionysus] Outbox worker"
|
101
|
+
end
|
102
|
+
|
103
|
+
def instrument(*args, **kwargs)
|
104
|
+
Dionysus.monitor.instrument(*args, **kwargs) do
|
105
|
+
yield if block_given?
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def tracer
|
110
|
+
@tracer ||= if Object.const_defined?(:Datadog)
|
111
|
+
Dionysus::Producer::Outbox::DatadogTracer.new
|
112
|
+
else
|
113
|
+
Dionysus::Utils::NullTracer
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def producer
|
118
|
+
@producer ||= Dionysus::Producer::Outbox::Producer.new
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Dionysus::Producer::Outbox::TombstonePublisher
|
4
|
+
TOMBSTONE = nil
|
5
|
+
private_constant :TOMBSTONE
|
6
|
+
|
7
|
+
attr_reader :config
|
8
|
+
private :config
|
9
|
+
|
10
|
+
def initialize(config: Dionysus::Producer.configuration)
|
11
|
+
@config = config
|
12
|
+
end
|
13
|
+
|
14
|
+
def tombstone(resource, responder, options = {})
|
15
|
+
partition_key = options.fetch(:partition_key) do
|
16
|
+
Dionysus::Producer::PartitionKey.new(resource, config: config).to_key(responder: responder)
|
17
|
+
end
|
18
|
+
key = options.fetch(:key) { Dionysus::Producer::Key.new(resource).to_key }
|
19
|
+
|
20
|
+
responder.call(TOMBSTONE, partition_key: partition_key, key: key)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Dionysus::Producer::Outbox
|
4
|
+
attr_reader :outbox_model, :config
|
5
|
+
private :outbox_model, :config
|
6
|
+
|
7
|
+
def initialize(outbox_model, config:)
|
8
|
+
@outbox_model = outbox_model
|
9
|
+
@config = config
|
10
|
+
end
|
11
|
+
|
12
|
+
def insert_created(record)
|
13
|
+
insert(Publishable.new(record), :created)
|
14
|
+
end
|
15
|
+
|
16
|
+
def insert_updated(record)
|
17
|
+
publishable = Publishable.new(record)
|
18
|
+
return [] unless publishable.previously_changed?
|
19
|
+
|
20
|
+
if publishable.respond_to?(config.soft_delete_column)
|
21
|
+
event_type = event_type_for_update_of_soft_deletable_record(publishable) or return []
|
22
|
+
insert(publishable, event_type)
|
23
|
+
else
|
24
|
+
insert(publishable, :updated)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def insert_destroyed(record)
|
29
|
+
publishable = Publishable.new(record)
|
30
|
+
|
31
|
+
insert(publishable, :destroyed)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
delegate :transaction_provider, :transactional_outbox_enabled, to: :config
|
37
|
+
|
38
|
+
def insert(publishable, event_type)
|
39
|
+
return [] unless transactional_outbox_enabled
|
40
|
+
|
41
|
+
transaction_provider.transaction do
|
42
|
+
publishable.topics.map do |topic|
|
43
|
+
attributes = {
|
44
|
+
resource: publishable.model,
|
45
|
+
event_name: event_name_for(publishable, event_type),
|
46
|
+
partition_key: partition_key_for_publishable_for_topic(publishable, topic),
|
47
|
+
topic: topic
|
48
|
+
}
|
49
|
+
attributes[:changeset] = publishable.changeset if observer_topic?(topic)
|
50
|
+
|
51
|
+
outbox_model.create!(attributes)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def partition_key_for_publishable_for_topic(publishable, topic)
|
57
|
+
responder = Dionysus::Producer
|
58
|
+
.responders_for_model_for_topic(publishable.model_class, topic)
|
59
|
+
.first or return
|
60
|
+
|
61
|
+
Dionysus::Producer::PartitionKey.new(publishable).to_key(responder: responder)
|
62
|
+
end
|
63
|
+
|
64
|
+
def observer_topic?(topic)
|
65
|
+
topic == Dionysus::Producer::Outbox::Model.observer_topic
|
66
|
+
end
|
67
|
+
|
68
|
+
def event_name_for(publishable, event_type)
|
69
|
+
Dionysus::Producer::Outbox::EventName
|
70
|
+
.new(publishable.resource_name)
|
71
|
+
.for_event_type(event_type)
|
72
|
+
end
|
73
|
+
|
74
|
+
def event_type_for_update_of_soft_deletable_record(publishable)
|
75
|
+
if publishable.previous_changes_include_canceled?
|
76
|
+
event_type_for_update_of_soft_deletable_record_for_soft_delete_state_change(publishable)
|
77
|
+
else
|
78
|
+
event_type_for_update_of_soft_deletable_record_for_standard_state_change(publishable)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def event_type_for_update_of_soft_deletable_record_for_soft_delete_state_change(publishable)
|
83
|
+
if publishable.previous_changes_uncanceled?
|
84
|
+
:created
|
85
|
+
elsif publishable.previous_changes_canceled?
|
86
|
+
:destroyed
|
87
|
+
elsif publishable.previous_changes_still_canceled? || publishable.previous_changed_still_visible?
|
88
|
+
nil
|
89
|
+
else
|
90
|
+
raise "that should never happen"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def event_type_for_update_of_soft_deletable_record_for_standard_state_change(publishable)
|
95
|
+
if publishable.visible? || (publishable.soft_deleted? && publishable.dionysus_publish_updates_after_soft_delete?)
|
96
|
+
:updated
|
97
|
+
elsif publishable.soft_deleted?
|
98
|
+
nil
|
99
|
+
else
|
100
|
+
raise "that should never happen"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|