smplkit 3.0.135 → 3.0.136

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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/smplkit/event_stream.rb +39 -9
  3. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c1cba1222e1ec1109993202959fa417b65752d969da90230f6f4ed913de9753
4
- data.tar.gz: d1656093905daa0e565c6fb20c482b922251d2a4eb0dfbc7ca8684bc323abac5
3
+ metadata.gz: 82238262a8b9ee5b4d6a01d678238c0b47f5b16b029d2af72f7452d39b522040
4
+ data.tar.gz: e7fa9dc7b95404fd98661942c5cce79d538d2b6812cd0245cd3bf8617316a821
5
5
  SHA512:
6
- metadata.gz: 4c18d2ec862ee1ad4b58539f326aaa55e6868de62205fe037998a00f90cb6d3c60676d80ab3f94771d8923d9c08543133d861358dc9c18dd1ff35a57ae15e783
7
- data.tar.gz: d9225dd299dbbeac794f58a7fbd48713e10166e4f7e266a4eedd4ead1649c7315542de018a5d1bfe1aa1796cb7f82d4c04bfac4fdab30ab1ecd21ac4ba48ad2c
6
+ metadata.gz: 38eb4b4763f7a80130e7160d4736176782f55161624e4b2fddfc514bd8155df49820ba83710e2a3cf2eb02ab2856d7f453e05c5c96302657d3393700eb9aa650
7
+ data.tar.gz: 149450fce5a3dc5da39c26a3bed0c191bae710a6541acf5ce967062008d72c627c066354019eece249d8da34da730cef9c258d14bafb528f0a5e29393356d453
@@ -35,14 +35,23 @@ module Smplkit
35
35
  #
36
36
  # On disconnect the reactor reconnects with exponential backoff (base
37
37
  # delay doubling up to +MAX_BACKOFF+ seconds), resetting to the base on
38
- # every successful connect. +stop+ closes the stream from the outer
39
- # thread; the reader exits and the daemon thread terminates.
38
+ # every successful connect. +stop+ flips +@closed+ and waits; the reader
39
+ # re-checks the flag at least every +POLL_INTERVAL+, tears its own
40
+ # connection down in-reactor, and the daemon thread terminates.
40
41
  class EventStream
41
42
  # Seconds without any bytes from the server (events or keepalive
42
43
  # comments) before the connection is considered dead — two missed
43
44
  # 30-second server keepalives.
44
45
  READ_TIMEOUT = 45
45
46
 
47
+ # How long a single blocking read may park the reactor before it wakes
48
+ # to re-check +@closed+. Liveness is tracked as a deadline across
49
+ # polls (see +read_loop+), so this changes nothing on the wire — it
50
+ # exists so +stop+ never has to interrupt the reactor from a foreign
51
+ # thread: a cross-thread close cannot wake a fiber blocked in
52
+ # +body.read+, which left teardown hanging until the next keepalive.
53
+ POLL_INTERVAL = 1.0
54
+
46
55
  # Ceiling for the exponential reconnect backoff, in seconds.
47
56
  MAX_BACKOFF = 60
48
57
 
@@ -254,12 +263,19 @@ module Smplkit
254
263
  def stop
255
264
  Smplkit.debug("events", "stopping shared event stream")
256
265
  @closed = true
257
- close_active_connection
258
266
  thread = @stream_thread
259
267
  @stream_thread = nil
260
268
  if thread
261
- thread.join(2.0)
262
- thread.kill if thread.alive?
269
+ # The reactor re-checks +@closed+ at least every POLL_INTERVAL and
270
+ # closes its own connection in-reactor on the way out — closing it
271
+ # from this thread instead would race the reactor and cannot wake
272
+ # a blocked read.
273
+ thread.join(POLL_INTERVAL + 1.0)
274
+ if thread.alive?
275
+ # Last resort: a connect attempt wedged before the read loop.
276
+ thread.kill
277
+ close_active_connection
278
+ end
263
279
  end
264
280
  # Set authoritatively after the thread is dead so a racing connect
265
281
  # call (which also sets "connecting") cannot clobber this value.
@@ -405,19 +421,33 @@ module Smplkit
405
421
  end
406
422
 
407
423
  # Read chunks until EOF, feeding the SSE parser and dispatching the
408
- # events it completes. Each read is individually wrapped in the liveness
409
- # timeout, so any received bytes including keepalive comment frames
410
- # reset the clock.
424
+ # events it completes. Reads park for at most POLL_INTERVAL at a time
425
+ # so +stop+ is honored promptly; liveness is a rolling deadline any
426
+ # received bytes, including keepalive comment frames, push it out by
427
+ # READ_TIMEOUT. A deadline breach raises into the reconnect path.
411
428
  def read_loop(task, body)
412
429
  parser = Parser.new
430
+ deadline = monotonic_now + READ_TIMEOUT
413
431
  until @closed
414
- chunk = task.with_timeout(READ_TIMEOUT) { body.read }
432
+ begin
433
+ chunk = task.with_timeout(POLL_INTERVAL) { body.read }
434
+ rescue Async::TimeoutError
435
+ raise Async::TimeoutError, "no data for #{READ_TIMEOUT}s" if monotonic_now >= deadline
436
+
437
+ next
438
+ end
415
439
  break if chunk.nil?
416
440
 
441
+ deadline = monotonic_now + READ_TIMEOUT
417
442
  process_chunk(parser, chunk)
418
443
  end
419
444
  end
420
445
 
446
+ # Seam for specs; the liveness deadline math needs a controllable clock.
447
+ def monotonic_now
448
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
449
+ end
450
+
421
451
  def process_chunk(parser, chunk)
422
452
  parser.feed(chunk).each { |event| handle_event(event.name, event.data) }
423
453
  @retry_base = parser.retry_ms / 1000.0 unless parser.retry_ms.nil?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smplkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.135
4
+ version: 3.0.136
5
5
  platform: ruby
6
6
  authors:
7
7
  - Smpl Solutions LLC