protobuf-nats 0.13.1 → 0.13.2.pre1

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: aeb96c5f90852f8dfdf32450fe5290325684cec82dbb88bd7b7e393db09cb6bb
4
- data.tar.gz: b577ae715c8fc9669b815510f0e2032b9454e9aef63c58731590c2fe2c3152c0
3
+ metadata.gz: 89549ac1d866b16970644740b1da683f985b150db9c03ed5a06d5c20eb81d77b
4
+ data.tar.gz: 3908346e349fb6e8c95ba13e89b5fa31872dc38be7a6103ad5bd327a19806062
5
5
  SHA512:
6
- metadata.gz: 81fe350143450675623252856ce7fe30253e3204699c9a2a6dbe8c7c270ea381567d55d3b7bde2b2c17078c64e46700a5f35a75f660bcc7b6f3e81b5fc4ea5b9
7
- data.tar.gz: f8c98030a76607518ecfdce5c47a9f2cba2d2ab52ffddfd46a75e0d173e5d62203b122fe747aacd805245815ae4901c1625f7147d5f4228f0497f94ea2f41aec
6
+ metadata.gz: 78886bd247470672da944c13f90b538e6913bf5378677bb3da5dc0ac48ee3f501e582a40d6501c8ae0b41ef66dc6368626a79d852a17929ca6739acdc27bd378
7
+ data.tar.gz: ba529ee4a32f3c4360d22bbd52a8bb04a175b97684fe8c5ca8b8e65b5a11d6f3a04fceaf5415872b951cc373f284146b76422bbe7eff467fa52c264af878f47d
@@ -13,6 +13,16 @@ module Protobuf
13
13
  MAX_RESPONSES_PER_TOKEN = 10
14
14
  TOKEN_TTL_SECONDS = 600 # 10 minutes
15
15
 
16
+ # Sentinel pushed onto a token's queue to wake a waiter blocked in
17
+ # next_message. We cannot rely on Queue#close alone: on JRuby, close does
18
+ # NOT wake a pop() that is blocked with a timeout: -- neither the native
19
+ # Queue (Ruby >= 3.2 / JRuby 10) nor concurrent-ruby's RubyTimeoutQueue
20
+ # (Ruby < 3.2 / JRuby 9.4, whose timed pop only wakes on push) signals a
21
+ # timed waiter on close. CRuby's Queue#close does wake it, which is why
22
+ # this only ever bit JRuby. Pushing an explicit sentinel wakes the waiter
23
+ # immediately on every engine; next_message treats it as a timeout.
24
+ QUEUE_WAKE = ::Object.new
25
+
16
26
  def initialize
17
27
  # Per-token response queues for lock-free message delivery. @resp_map is a
18
28
  # Concurrent::Map so request threads and dispatcher threads can insert,
@@ -66,9 +76,24 @@ module Protobuf
66
76
  end
67
77
 
68
78
  def cleanup(token)
69
- # Atomic remove-and-return; close the queue to wake any waiting threads.
79
+ # Atomic remove-and-return; wake+close the queue to release any waiter.
70
80
  entry = @resp_map.delete(token)
71
- entry[:queue]&.close if entry
81
+ wake_and_close_queue(entry[:queue]) if entry
82
+ end
83
+
84
+ # Wake any waiter blocked in next_message on this queue, then close it.
85
+ # Pushing QUEUE_WAKE is what actually wakes a timed pop on JRuby (see the
86
+ # QUEUE_WAKE comment); close alone is insufficient there. Safe to call on
87
+ # an already-closed queue.
88
+ def wake_and_close_queue(queue)
89
+ return unless queue
90
+ begin
91
+ queue.push(QUEUE_WAKE)
92
+ rescue ::ClosedQueueError, ::ThreadError
93
+ # Already closed by another path; a plain (untimed) waiter, if any,
94
+ # was already woken by that close. Nothing more to do.
95
+ end
96
+ queue.close
72
97
  end
73
98
 
74
99
  def next_message(token, timeout)
@@ -101,7 +126,10 @@ module Protobuf
101
126
  # Queue.pop returns nil when:
102
127
  # 1. The queue is closed
103
128
  # 2. The timeout expires
104
- unless msg
129
+ # QUEUE_WAKE is the sentinel pushed by wake_and_close_queue to wake a
130
+ # timed pop on JRuby (where close alone does not); treat it as a
131
+ # timeout so the caller fails over instead of returning garbage.
132
+ if msg.nil? || msg.equal?(QUEUE_WAKE)
105
133
  logger.warn "Queue closed or timeout for token #{token} during next_message"
106
134
  raise ::NATS::Timeout
107
135
  end
@@ -293,8 +321,8 @@ module Protobuf
293
321
  next unless data
294
322
  stale_count += 1
295
323
  logger.warn "Cleaning up stale token #{token} created at #{data[:created_at]}"
296
- # Close the queue to wake any waiting threads
297
- data[:queue]&.close
324
+ # Wake any waiting thread, then close the queue.
325
+ wake_and_close_queue(data[:queue])
298
326
  end
299
327
 
300
328
  if stale_count > 0
@@ -348,7 +376,7 @@ module Protobuf
348
376
  # Must be called while holding LOCK (only from drop_subscription_locked).
349
377
  def fail_inflight_requests
350
378
  @resp_map.each_pair do |_token, entry|
351
- entry[:queue]&.close
379
+ wake_and_close_queue(entry[:queue])
352
380
  end
353
381
  end
354
382
 
@@ -89,14 +89,12 @@ module Protobuf
89
89
  break
90
90
  end
91
91
 
92
- # Non-blocking push with timeout
93
- begin
94
- Timeout.timeout(1) do
95
- @pending_queue << msg
96
- end
92
+ # Push with a deadline (see push_with_deadline: no Timeout.timeout,
93
+ # which corrupts the SizedQueue mutex on JRuby).
94
+ if push_with_deadline(msg, 1)
97
95
  migrated_count += 1
98
96
  logger.warn "Migrated message #{migrated_count} from old queue to central queue"
99
- rescue Timeout::Error
97
+ else
100
98
  logger.error "Failed to migrate message to central queue (queue full), dropping message"
101
99
  break
102
100
  end
@@ -117,15 +115,15 @@ module Protobuf
117
115
 
118
116
  # Wake every handler with its own poison pill.
119
117
  handlers.size.times do
120
- begin
121
- # Clear some space if the queue is full so the shutdown signal fits.
122
- if @pending_queue.num_waiting.zero? && @pending_queue.size >= @pending_queue.max
123
- logger.warn "Queue full during shutdown, clearing to make room for shutdown signal"
124
- @pending_queue.clear rescue nil
125
- end
118
+ # Clear some space if the queue is full so the shutdown signal fits.
119
+ if @pending_queue.num_waiting.zero? && @pending_queue.size >= @pending_queue.max
120
+ logger.warn "Queue full during shutdown, clearing to make room for shutdown signal"
121
+ @pending_queue.clear rescue nil
122
+ end
126
123
 
127
- Timeout.timeout(1) { @pending_queue << :shutdown }
128
- rescue Timeout::Error
124
+ # Push with a deadline (see push_with_deadline: no Timeout.timeout,
125
+ # which corrupts the SizedQueue mutex on JRuby).
126
+ unless push_with_deadline(:shutdown, 1)
129
127
  logger.error "Failed to send shutdown signal (queue blocked); will force-kill remaining handlers"
130
128
  break
131
129
  end
@@ -178,6 +176,32 @@ module Protobuf
178
176
  ::Protobuf::Nats.monotonic_time
179
177
  end
180
178
 
179
+ # Push onto the shared SizedQueue with a deadline, WITHOUT Timeout.timeout.
180
+ # Timeout uses an asynchronous Thread#raise, which is unsafe around the
181
+ # mutex SizedQueue#push takes internally: on JRuby (10.x in particular) a
182
+ # timeout firing mid-push unwinds through the held mutex and raises
183
+ # "ThreadError: Attempt to unlock a mutex which is locked by another
184
+ # thread/fiber" instead of Timeout::Error -- so the rescue :Timeout::Error
185
+ # never fires and shutdown/migration blow up. CRuby happens to unwind
186
+ # cleanly, which is why this only bit JRuby. Poll a non-blocking push
187
+ # against a monotonic deadline instead: no async raise, safe on every
188
+ # engine. Returns true if pushed, false if the deadline passed (queue
189
+ # still full) or the queue was closed.
190
+ def push_with_deadline(obj, timeout)
191
+ deadline = monotonic + timeout
192
+ loop do
193
+ begin
194
+ @pending_queue.push(obj, true) # non_block: raises ThreadError when full
195
+ return true
196
+ rescue ::ClosedQueueError
197
+ return false
198
+ rescue ::ThreadError
199
+ return false if monotonic >= deadline
200
+ sleep 0.01
201
+ end
202
+ end
203
+ end
204
+
181
205
  # Spawn one intake handler. Each thread owns its own crash_count so the
182
206
  # self-healing exponential backoff is correct under true parallelism (a
183
207
  # shared counter would lose updates across handlers on JRuby). The counter
@@ -1,5 +1,5 @@
1
1
  module Protobuf
2
2
  module Nats
3
- VERSION = "0.13.1"
3
+ VERSION = "0.13.2.pre1"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protobuf-nats
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.1
4
+ version: 0.13.2.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandon Dewitt
@@ -183,9 +183,7 @@ extensions: []
183
183
  extra_rdoc_files: []
184
184
  files:
185
185
  - ".circleci/config.yml"
186
- - ".github/workflows/ci.yml"
187
186
  - ".gitignore"
188
- - ".travis.yml"
189
187
  - CHANGELOG.md
190
188
  - Gemfile
191
189
  - LICENSE.txt
@@ -1,64 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [master]
6
- pull_request:
7
-
8
- jobs:
9
- test:
10
- runs-on: ubuntu-latest
11
- strategy:
12
- fail-fast: false
13
- matrix:
14
- ruby:
15
- - "3.1"
16
- - "3.4"
17
- - "jruby-9.4"
18
- - "jruby-10.0"
19
-
20
- # Real NATS server for the integration specs (spec/integration/); they
21
- # auto-detect it on localhost:4222 (see spec/spec_helper.rb).
22
- services:
23
- nats:
24
- image: nats:2.14-linux
25
- ports:
26
- - 4222:4222
27
-
28
- env:
29
- JRUBY_OPTS: "-J-Xmx1024m"
30
- RAILS_ENV: test
31
-
32
- steps:
33
- - uses: actions/checkout@v4
34
-
35
- - uses: ruby/setup-ruby@v1
36
- with:
37
- ruby-version: ${{ matrix.ruby }}
38
- bundler-cache: true
39
-
40
- # The cluster failover spec (spec/integration/failover_spec.rb) spawns
41
- # its own two-node cluster, so it needs the nats-server binary itself --
42
- # the service container above only covers the single-node specs.
43
- - name: Install nats-server binary (cluster failover spec)
44
- run: |
45
- NATS_VERSION=v2.14.3
46
- curl -sfL "https://github.com/nats-io/nats-server/releases/download/${NATS_VERSION}/nats-server-${NATS_VERSION}-linux-amd64.tar.gz" | tar xz
47
- sudo mv "nats-server-${NATS_VERSION}-linux-amd64/nats-server" /usr/local/bin/
48
- nats-server --version
49
-
50
- - name: Wait for NATS
51
- run: |
52
- echo "Waiting for NATS to start..."
53
- for i in $(seq 1 30); do
54
- if nc -z localhost 4222; then
55
- echo "NATS is ready!"
56
- exit 0
57
- fi
58
- sleep 1
59
- done
60
- echo "NATS did not become ready" >&2
61
- exit 1
62
-
63
- - name: Run tests
64
- run: bundle exec rspec
data/.travis.yml DELETED
@@ -1,16 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- jdk:
4
- - openjdk8
5
- rvm:
6
- - 2.3.0
7
- - 2.7.0
8
- - jruby-9.1.7.0
9
- - jruby-9.2.13.0
10
- before_install:
11
- # Install and start gnatsd
12
- - ./scripts/install_gnatsd.sh
13
- - $HOME/nats-server/nats-server &
14
- # Install deps for project
15
- - gem install bundler
16
- - gem update --system