dionysus-rb 1.2.0 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 715bda096eaf87ea615134206607a11d22ff3d673b5cbe9625c584599fb5696b
4
- data.tar.gz: d4c5741f509d02f0c5a28a4332f34b08b28f46ca1c7d026d59f59bff2e37638f
3
+ metadata.gz: 968407d443681c04bfc7426b8925d7f63db69a09763357413d01218c03ec3ad8
4
+ data.tar.gz: baf5e739a77bc2d48ab5197591ddb59c6d970bdaed68479014d5afbec5698f0e
5
5
  SHA512:
6
- metadata.gz: c101ce0cfa0f3ddb58339bdf28b3b479838fb3b66107d8223d4a795888e5048cc8f044db232fe7ae03899cf558d1b8100a8119119d8a8d3faac3996082671618
7
- data.tar.gz: be3c63dddbc4b1c3ae58134b050370c62a0940d3227ca3e1820cce72a1bba36cb932de2906d42a7fe723f811ac30dbc4431f668c5832787fd0a27fb521fc5594
6
+ metadata.gz: 121b9c6af2b90700a7af1aba098282cfe05df901c633dfcc71d7845bda935b5e124b21c8bedab20895e9c5dd11a3dd12c2edd049c1bcd9b984c7df0da8a246b5
7
+ data.tar.gz: 6594b49acf6f64bb59475bc4a8a3a70c5747a09dcb72d3bc815e544bf3016bf0df732f3b15f0b3b7c8106578ba3c8ed49f4206d5b206e240285d376b6e08b31f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.3.0]
4
+
5
+ ### Fixed
6
+
7
+ - Read the record and its associations without the query cache while publishing, behind `config.publish_with_uncached_reads` (default `false`). The payload is built from the record plus the associations declared with `with:`. When `publish_after_commit` is on, publishing runs inside the request, where Rails' query cache is live on that connection - so a row read earlier in the request is served from the cache while its associations are queried fresh. The payload then carries an `updated_at` older than the data it contains.
8
+ - Measured in production on the sibling gem over a two and a half hour window on one topic: of 1,476 records that received more than one message, 141 had a payload whose `updated_at` moved backwards as `serialized_at` moved forwards, and in 69 of those the message that wins deduplication carried an `updated_at` lower than one already published at a lower offset. One record published a payload serialized at `09:27:23.662` reporting `updated_at 09:27:22.139557`, when a payload serialized 1.3 seconds earlier had already reported `09:27:22.345092` - a value a row's `updated_at` cannot return to.
9
+ - The consequence is not a stale field but a discarded record. Consumers rank duplicates and then apply `persist_with_dionysus?`, which compares the payload's `updated_at` against what is stored; a payload that loses that comparison is skipped by `next`, and the `has_many` records embedded in it are persisted further down the same loop body, so they are dropped with it. The parent keeps its old values and the new child rows are never created, with no error, no counter and nothing to self-heal it.
10
+ - This is why no ordering over the fields already in the message could fix both known incidents: `updated_at` was in the message all along and is simply wrong. `serialized_at`, added in 1.2.0, correctly identifies the last-serialized message - which is precisely the one the guard rejects when its `updated_at` is stale.
11
+ - Rolling this out needs no consumer changes: the fix is entirely on the producer, and the payload's shape is unchanged.
12
+
3
13
  ## [1.2.0]
4
14
 
5
15
  ### Fixed
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dionysus-rb (1.2.0)
4
+ dionysus-rb (1.3.0)
5
5
  activerecord (>= 5)
6
6
  activesupport (>= 3.2)
7
7
  concurrent-ruby
@@ -9,7 +9,7 @@ class Dionysus::Producer::Config
9
9
  :transactional_outbox_enabled, :sidekiq_queue, :publisher_service_name,
10
10
  :genesis_consistency_safety_delay, :hermes_event_producer, :publish_after_commit, :outbox_worker_publishing_delay,
11
11
  :high_priority_sidekiq_queue, :observers_inline_maximum_size, :remove_consecutive_duplicates_before_publishing,
12
- :include_serialized_at_in_payload
12
+ :include_serialized_at_in_payload, :publish_with_uncached_reads
13
13
 
14
14
  def self.default_sidekiq_queue
15
15
  :dionysus
@@ -97,6 +97,14 @@ class Dionysus::Producer::Config
97
97
  @observers_inline_maximum_size || 1000
98
98
  end
99
99
 
100
+ # Off by default so it can be switched on per producer, and switched back off in one env change
101
+ # if the extra reads ever cost more than they are worth.
102
+ def publish_with_uncached_reads
103
+ return @publish_with_uncached_reads if defined?(@publish_with_uncached_reads)
104
+
105
+ false
106
+ end
107
+
100
108
  # Off by default so consumers, which fall back to the offset when the field is absent, can be
101
109
  # rolled out first.
102
110
  def include_serialized_at_in_payload
@@ -11,52 +11,56 @@ class Dionysus::Producer::Outbox::Publisher
11
11
  def publish(outbox_record, options = {})
12
12
  return if Dionysus::Producer::Suppressor.suppressed?
13
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
14
+ with_consistent_reads do
15
+ instrument("publishing_with_dionysus") do
16
+ resource_class = outbox_record.resource_class.constantize
17
+ primary_key = resource_class.primary_key
18
+ primary_key_value = outbox_record.resource_id
19
+ topic = outbox_record.topic
20
+ resource = resource_class.find_by(primary_key => primary_key_value) ||
21
+ resource_class.new(primary_key => primary_key_value)
22
+ event_name = outbox_record.event_name
23
+ if resource.new_record? && outbox_record.created_event?
24
+ logger.error(
25
+ "Attempted to publish #{resource.class}, id: #{resource.id} but it was deleted, that should never happen!"
26
+ )
27
+ return
28
+ end
28
29
 
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
30
+ if resource.new_record? && outbox_record.updated_event?
31
+ logger.error(
32
+ "There was an update of #{resource.class}, id: #{resource.id} but it was deleted, that should never happen!"
33
+ )
34
+ return
35
+ end
35
36
 
36
- published_count = publish_for_top_level_resource(outbox_record, resource, event_name, topic, options) +
37
- publish_for_dependency(resource, topic, options)
38
- handle_nothing_published(resource, event_name, topic) if published_count.zero?
39
- published_count
37
+ published_count = publish_for_top_level_resource(outbox_record, resource, event_name, topic, options) +
38
+ publish_for_dependency(resource, topic, options)
39
+ handle_nothing_published(resource, event_name, topic) if published_count.zero?
40
+ published_count
41
+ end
40
42
  end
41
43
  end
42
44
 
43
45
  def publish_observers(outbox_record)
44
46
  return if Dionysus::Producer::Suppressor.suppressed?
45
47
 
46
- instrument("publishing_observers_with_dionysus") do
47
- resource_class = outbox_record.resource_class.constantize
48
- primary_key = resource_class.primary_key
49
- primary_key_value = outbox_record.resource_id
50
- resource = resource_class.find_by(primary_key => primary_key_value) ||
51
- resource_class.new(primary_key => primary_key_value)
52
- changeset = outbox_record.transformed_changeset
53
-
54
- Dionysus::Producer.observers_with_responders_for(resource,
55
- changeset).each do |observers, responder|
56
- if observers.count > config.observers_inline_maximum_size
57
- execute_genesis_for_observers(observers, responder)
58
- else
59
- observers.each { |observer_record| publish_observer(observer_record, responder) }
48
+ with_consistent_reads do
49
+ instrument("publishing_observers_with_dionysus") do
50
+ resource_class = outbox_record.resource_class.constantize
51
+ primary_key = resource_class.primary_key
52
+ primary_key_value = outbox_record.resource_id
53
+ resource = resource_class.find_by(primary_key => primary_key_value) ||
54
+ resource_class.new(primary_key => primary_key_value)
55
+ changeset = outbox_record.transformed_changeset
56
+
57
+ Dionysus::Producer.observers_with_responders_for(resource,
58
+ changeset).each do |observers, responder|
59
+ if observers.count > config.observers_inline_maximum_size
60
+ execute_genesis_for_observers(observers, responder)
61
+ else
62
+ observers.each { |observer_record| publish_observer(observer_record, responder) }
63
+ end
60
64
  end
61
65
  end
62
66
  end
@@ -64,6 +68,18 @@ class Dionysus::Producer::Outbox::Publisher
64
68
 
65
69
  private
66
70
 
71
+ # The payload is built from the record and its associations. Publishing runs inside the request
72
+ # when publish_after_commit is on, where Rails' query cache is live on that connection, so a row
73
+ # read earlier in the request can be served from the cache while its associations are queried
74
+ # fresh - producing a payload whose updated_at is older than the data it carries. Consumers rank
75
+ # and guard on updated_at, so such a payload is silently discarded, taking its embedded
76
+ # associations with it.
77
+ def with_consistent_reads(&)
78
+ return yield unless config.publish_with_uncached_reads
79
+
80
+ ActiveRecord::Base.uncached(&)
81
+ end
82
+
67
83
  delegate :instrumenter, :error_handler, to: :config
68
84
  delegate :instrument, to: :instrumenter
69
85
  delegate :logger, to: Dionysus
@@ -3,5 +3,5 @@
3
3
  module Dionysus
4
4
  module Version
5
5
  end
6
- VERSION = "1.2.0"
6
+ VERSION = "1.3.0"
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dionysus-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Karol Galanciak
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-26 00:00:00.000000000 Z
11
+ date: 2026-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord