dionysus-rb 1.4.0 → 1.4.1

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: 8cd74dab3609a68e4919adb2913c5e2d428b2bd044ec785988eae123d125677b
4
- data.tar.gz: 15ae10c89ce12d63f0d452bf8ebfb835a8b3593784048c003a28a093a9c77250
3
+ metadata.gz: aea1780da49737ea39a412112502ccefec1921bcaa8e4d9e3f4a17c01e359cb1
4
+ data.tar.gz: 0de793797246fa1aa63209eb9713529df3bf3b51aedca2d03be485ab3b140f43
5
5
  SHA512:
6
- metadata.gz: f3a2a0e0179804ff474dc9bb7575ba565e3c4377f6755cfb31aed5c783b7a3915966af0158a50132155a18c588e44495540d47a7463e475d07260f9db4cea6ef
7
- data.tar.gz: 2e4450ae8743746067d81ab3c28614c4293fec570fdb550a9240b44f4e8ed097d7f3ecc110e03f51135c8fcd8237835e0a5a3ca90f754009b9146bd2e83e4866
6
+ metadata.gz: 9ba62b20fdf34088c43f28bfd71e4fa721b4d512125ca030c16c61eb077354aab5751a05c1ce0192708a7fc87ccd3d3af0a63b8a2f06a48ab903858c928d4ce8
7
+ data.tar.gz: d67700b960048a4ea6b272d20d193ba4232115665a5a72d0b2171cec6ba089c3bb451ab36fd5092ffbec55a763fca95fa7b8b7299b3f44263122e58aea26932f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.4.1]
4
+ - Stamp `serialized_at` when the attempt that is actually published begins, not before the retry loop. 1.4.0 takes the stamp once and then re-serializes up to `max_snapshot_attempts` times, so a message can be published a whole retry cycle after its own stamp - and the payload it publishes is the one read last, not the one the stamp describes. Consumers rank duplicates by `[serialized_at, offset]`, so the freshest payload ended up carrying the oldest stamp and losing its group to a staler sibling. That is the failure `serialized_at` was added to prevent, reintroduced by the guard added to prevent a different one.
5
+ - Measured on the affected topic over 65 minutes: 205 of 6,572 messages (3.12%) carried an `updated_at` LATER than their own `serialized_at`, the worst by 1,225 ms. A timestamp cannot describe a moment after the one it was taken at, so that alone proves the stamp preceded the payload.
6
+ - `serialize_consistently` now returns `[payload, read_at]`. The looseness between stamp and payload is bounded by a single serialization pass again, as it was before 1.4.0, rather than by the whole retry cycle - so contended records, which retry most, are no longer the ones stamped most wrongly.
7
+ - Covered end to end against a real broker in `spec/serialized_at_tracks_published_attempt_spec.rb`: a contended publish retries while a competing publish serializes once and stamps later, both are read back off the topic, and the real `RemoveDuplicatesStrategy` is asked which survives. The spec fails on 1.4.0 on both assertions.
8
+
3
9
  ## [1.4.0]
4
10
 
5
11
  ### Fixed
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dionysus-rb (1.4.0)
4
+ dionysus-rb (1.4.1)
5
5
  activerecord (>= 5)
6
6
  activesupport (>= 3.2)
7
7
  concurrent-ruby
@@ -46,10 +46,10 @@ class Dionysus::Producer::KarafkaResponderGenerator
46
46
 
47
47
  record = records.sample
48
48
 
49
- # the offset alone is publish order, and a message published later can carry an
50
- # earlier snapshot, so consumers need to know when this payload was actually read
51
- serialized_at = config.include_serialized_at_in_payload ? Time.now.utc : nil
52
- payload = serialize_consistently(records, topic, batch_options)
49
+ # consumers rank duplicates by this stamp, so it has to describe the attempt actually
50
+ # published - with retries that is the last one, not the first
51
+ payload, read_at = serialize_consistently(records, topic, batch_options)
52
+ serialized_at = config.include_serialized_at_in_payload ? read_at : nil
53
53
 
54
54
  event_payload = {
55
55
  event: event,
@@ -88,26 +88,33 @@ class Dionysus::Producer::KarafkaResponderGenerator
88
88
  # So serialize, then check whether the records moved underneath it. If they did, the payload
89
89
  # is not a snapshot of anything: reload and serialize again. Retries are bounded, and the last
90
90
  # attempt is published rather than dropped - a payload that may be torn still beats no message.
91
+ # returns [payload, read_at] - read_at is when the attempt that produced this payload began
91
92
  define_method :serialize_consistently do |records, current_topic, batch_options|
92
- next serialize_to_payload(records, current_topic, batch_options) unless config.publish_consistent_snapshots
93
+ unless config.publish_consistent_snapshots
94
+ read_at = Time.now.utc
95
+ next [serialize_to_payload(records, current_topic, batch_options), read_at]
96
+ end
93
97
 
94
98
  max_attempts = config.max_snapshot_attempts
95
99
  attempts = 0
96
100
  loop do
101
+ read_at = Time.now.utc
97
102
  before = snapshot_of(records)
98
103
  payload = serialize_to_payload(records, current_topic, batch_options)
99
104
  attempts += 1
100
105
 
101
- # nothing to compare against, so the guard did not run on this message at all
102
- break instrument_snapshot("unsupported", attempts, records, current_topic, payload) if before.nil?
106
+ if before.nil?
107
+ # nothing to compare against, so the guard did not run on this message at all
108
+ break [instrument_snapshot("unsupported", attempts, records, current_topic, payload), read_at]
109
+ end
103
110
  if before == committed_snapshot_of(records)
104
- break instrument_snapshot("consistent", attempts, records, current_topic, payload)
111
+ break [instrument_snapshot("consistent", attempts, records, current_topic, payload), read_at]
105
112
  end
106
113
  if attempts >= max_attempts
107
114
  # published anyway: a payload that may be torn beats no message. Nothing downstream can
108
115
  # tell this apart from a clean one - the payload carries no marker and the consumer sees
109
116
  # an ordinary message - so this counter is the only place the outcome is ever visible.
110
- break instrument_snapshot("exhausted", attempts, records, current_topic, payload)
117
+ break [instrument_snapshot("exhausted", attempts, records, current_topic, payload), read_at]
111
118
  end
112
119
 
113
120
  records.each { |record| record.reload if record.is_a?(ActiveRecord::Base) && record.persisted? }
@@ -3,5 +3,5 @@
3
3
  module Dionysus
4
4
  module Version
5
5
  end
6
- VERSION = "1.4.0"
6
+ VERSION = "1.4.1"
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.4.0
4
+ version: 1.4.1
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-28 00:00:00.000000000 Z
11
+ date: 2026-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord