nats-pure 2.5.0 → 2.6.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 +4 -4
- data/CHANGELOG.md +27 -0
- data/lib/nats/io/client.rb +173 -60
- data/lib/nats/io/jetstream/manager.rb +1 -1
- data/lib/nats/io/jetstream/msg/ack.rb +7 -3
- data/lib/nats/io/jetstream/msg/ack_methods.rb +6 -2
- data/lib/nats/io/jetstream/msg/metadata.rb +2 -1
- data/lib/nats/io/jetstream.rb +1 -1
- data/lib/nats/io/kv.rb +5 -10
- data/lib/nats/io/parser.rb +3 -3
- data/lib/nats/io/subscription.rb +8 -1
- data/lib/nats/io/version.rb +2 -2
- data/lib/nats/io/websocket.rb +32 -12
- data/lib/nats/service/validator.rb +2 -2
- metadata +3 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d5c22758be6db2c05382a55c718c2c4f836a2c723f77820d4728bbf1538f30b
|
|
4
|
+
data.tar.gz: 1745fb8cfa406b598099779a027a3cc3dcc4cee28c22fd7c08e1b156b6ce4a5b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 84b049092f60f34c3ca6b5a8ac2b4334a1a32dbaefd2659424f91db3472df105604bbddd4989793a56757a9cdee1c710db6b52fc02e40b95f64bc0b0eae21dd6
|
|
7
|
+
data.tar.gz: 38585616a5820faefe491c8c16d815bc52759e0cd2b7f77ab5b3a0227d3728720c4f1ac56d6d2af6354c2c53e54245ab48ce71c2a063efe3fe737e60e21c6af7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
1
|
# Change log
|
|
2
2
|
|
|
3
3
|
## main
|
|
4
|
+
|
|
5
|
+
## v2.6.0
|
|
6
|
+
|
|
7
|
+
This release prepares nats-pure for nats-server 2.16, whose new JetStream
|
|
8
|
+
acknowledgement format breaks JetStream consumers and KV watchers in earlier
|
|
9
|
+
versions, and fixes a deadlock that could hang `Client#close`.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- With `reconnect: false`, losing the connection now closes the client, as nats.go does: the status becomes `CLOSED`, the close callback is called, and `publish` raises `NATS::IO::ConnectionClosedError`. It used to stay `DISCONNECTED` and keep accepting publishes that were never sent, because the close, started from the read loop, killed the read loop part way through. (#189)
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- JetStream: accept the 11-token acknowledgement subjects that nats-server 2.16 sends by default (the v2 format of ADR-15). Earlier versions fail to read the metadata of every JetStream message from such a server, which breaks pull and push consumers and KV watchers. (#185)
|
|
18
|
+
- JetStream: an unrecognized acknowledgement subject raises `NATS::JetStream::Error::NotJSMessage` instead of `NameError`, and subjects whose numeric fields do not parse are rejected. (#185)
|
|
19
|
+
- JetStream: message metadata timestamps keep their full nanosecond precision. (#185)
|
|
20
|
+
- KV: watchers track their position from message metadata, so they resume after a server restart with either acknowledgement format. (#185)
|
|
21
|
+
- `Client#close` could hang forever. It stopped the client's background threads with `Thread#exit`, and killing a thread that is waiting for the client lock can lose Ruby's mutex wakeup, leaving `close` asleep on an unlocked lock. The threads are now asked to stop and joined, on close and on reconnect. (#183, #189)
|
|
22
|
+
- A close while a reconnect was under way did not stick: the reconnect carried on, connected again and restarted the client, subscriptions included. (#189)
|
|
23
|
+
- A subscription with a concurrency limit could run its callbacks in parallel, and so out of order: an empty pending queue released the concurrency permit twice. (#186)
|
|
24
|
+
- A subscription could stop processing messages for good after a reconnect: shutting down the executor during dispatch killed the read loop and leaked the concurrency permit. (#182)
|
|
25
|
+
- WebSocket: protocol lines sharing a frame, such as the `PONG` and `INFO` that nats-server 2.12.2 sends together, were misparsed (#174, thanks @artyomb), and bytes that arrived with the reply to the connect handshake were lost. (#182, #186)
|
|
26
|
+
- JRuby: a refused connection raises `Errno::ECONNREFUSED`, as on MRI. (#182, #186)
|
|
27
|
+
|
|
28
|
+
### Improved
|
|
29
|
+
|
|
30
|
+
- Tested against nats-server 2.14, 2.15 and the upcoming 2.16 (`main`), on Ruby 3.0 to 3.4 and JRuby. (#182, #188)
|
data/lib/nats/io/client.rb
CHANGED
|
@@ -221,6 +221,10 @@ module NATS
|
|
|
221
221
|
# Track whether connect has been already been called.
|
|
222
222
|
@connect_called = false
|
|
223
223
|
|
|
224
|
+
# Bumped by every close, so that a reconnect started earlier can tell
|
|
225
|
+
# that the connection was closed meanwhile.
|
|
226
|
+
@close_generation = 0
|
|
227
|
+
|
|
224
228
|
# New style request/response implementation.
|
|
225
229
|
@resp_sub = nil
|
|
226
230
|
@resp_map = nil
|
|
@@ -1293,6 +1297,11 @@ module NATS
|
|
|
1293
1297
|
# Handles errors from reading, parsing the protocol or stale connection.
|
|
1294
1298
|
# the lock should not be held entering this function.
|
|
1295
1299
|
def process_op_error(e)
|
|
1300
|
+
# A background thread being stopped by close or a reconnect sees
|
|
1301
|
+
# errors from that; it must not start another close or reconnect,
|
|
1302
|
+
# which would wait for the thread that is stopping it.
|
|
1303
|
+
return if stopping?
|
|
1304
|
+
|
|
1296
1305
|
should_bail = synchronize do
|
|
1297
1306
|
connecting? || closed? || reconnecting?
|
|
1298
1307
|
end
|
|
@@ -1320,23 +1329,29 @@ module NATS
|
|
|
1320
1329
|
|
|
1321
1330
|
def initiate_reconnect
|
|
1322
1331
|
@status = RECONNECTING
|
|
1323
|
-
@io
|
|
1332
|
+
old_io = @io
|
|
1324
1333
|
@io = nil
|
|
1325
1334
|
|
|
1335
|
+
# Wake a read loop blocked on the socket by shutting it down, and
|
|
1336
|
+
# close the socket only once the loop has stopped: on JRuby a close
|
|
1337
|
+
# right after the shutdown can leave the read blocked for good.
|
|
1338
|
+
if @read_loop_thread&.alive? && @read_loop_thread != Thread.current
|
|
1339
|
+
old_io&.shutdown_read
|
|
1340
|
+
end
|
|
1341
|
+
|
|
1326
1342
|
# TODO: Reconnecting pending buffer?
|
|
1327
1343
|
|
|
1344
|
+
generation = @close_generation
|
|
1345
|
+
|
|
1328
1346
|
# Do reconnect under a different thread than the one
|
|
1329
1347
|
# in which we got the error.
|
|
1330
1348
|
Thread.new do
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
@read_loop_thread.exit if @read_loop_thread.alive?
|
|
1334
|
-
@flusher_thread.exit if @flusher_thread.alive?
|
|
1335
|
-
@ping_interval_thread.exit if @ping_interval_thread.alive?
|
|
1349
|
+
stop_threads!
|
|
1350
|
+
old_io&.close
|
|
1336
1351
|
|
|
1337
1352
|
@subscription_executor.shutdown
|
|
1338
1353
|
|
|
1339
|
-
attempt_reconnect
|
|
1354
|
+
attempt_reconnect(generation)
|
|
1340
1355
|
rescue NATS::IO::NoServersError => e
|
|
1341
1356
|
@last_err = e
|
|
1342
1357
|
close
|
|
@@ -1346,6 +1361,8 @@ module NATS
|
|
|
1346
1361
|
# Gathers data from the socket and sends it to the parser.
|
|
1347
1362
|
def read_loop
|
|
1348
1363
|
loop do
|
|
1364
|
+
return if stopping?
|
|
1365
|
+
|
|
1349
1366
|
should_bail = synchronize do
|
|
1350
1367
|
# FIXME: In case of reconnect as well?
|
|
1351
1368
|
@status == CLOSED or @status == RECONNECTING
|
|
@@ -1361,6 +1378,9 @@ module NATS
|
|
|
1361
1378
|
# FIXME: We do not really need a timeout here...
|
|
1362
1379
|
retry
|
|
1363
1380
|
rescue => e
|
|
1381
|
+
# Asked to stop: the error comes from the socket being shut down.
|
|
1382
|
+
return if stopping?
|
|
1383
|
+
|
|
1364
1384
|
# In case of reading/parser errors, trigger
|
|
1365
1385
|
# reconnection logic in case desired.
|
|
1366
1386
|
process_op_error(e)
|
|
@@ -1373,6 +1393,7 @@ module NATS
|
|
|
1373
1393
|
loop do
|
|
1374
1394
|
# Blocks waiting for the flusher to be kicked...
|
|
1375
1395
|
@flush_queue.pop
|
|
1396
|
+
return if stopping?
|
|
1376
1397
|
|
|
1377
1398
|
should_bail = synchronize do
|
|
1378
1399
|
(@status != CONNECTED && !draining?) || @status == CONNECTING
|
|
@@ -1411,9 +1432,9 @@ module NATS
|
|
|
1411
1432
|
end
|
|
1412
1433
|
end
|
|
1413
1434
|
|
|
1414
|
-
def ping_interval_loop
|
|
1435
|
+
def ping_interval_loop(stop)
|
|
1415
1436
|
loop do
|
|
1416
|
-
|
|
1437
|
+
return if stop.wait(@options[:ping_interval])
|
|
1417
1438
|
|
|
1418
1439
|
# Skip ping interval until connected
|
|
1419
1440
|
next if !connected?
|
|
@@ -1486,8 +1507,11 @@ module NATS
|
|
|
1486
1507
|
end
|
|
1487
1508
|
end
|
|
1488
1509
|
|
|
1489
|
-
# Reconnect logic
|
|
1490
|
-
|
|
1510
|
+
# Reconnect logic. generation is @close_generation from when the
|
|
1511
|
+
# reconnect started; a close since then cancels the reconnect.
|
|
1512
|
+
def attempt_reconnect(generation)
|
|
1513
|
+
return if closed_since?(generation)
|
|
1514
|
+
|
|
1491
1515
|
@disconnect_cb&.call(@last_err)
|
|
1492
1516
|
|
|
1493
1517
|
# Clear sticky error
|
|
@@ -1496,6 +1520,8 @@ module NATS
|
|
|
1496
1520
|
# Do reconnect
|
|
1497
1521
|
srv = nil
|
|
1498
1522
|
begin
|
|
1523
|
+
return if closed_since?(generation)
|
|
1524
|
+
|
|
1499
1525
|
srv = select_next_server
|
|
1500
1526
|
|
|
1501
1527
|
# Set hostname to use for TLS hostname verification
|
|
@@ -1537,37 +1563,49 @@ module NATS
|
|
|
1537
1563
|
retry
|
|
1538
1564
|
end
|
|
1539
1565
|
|
|
1540
|
-
#
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1566
|
+
# Under the lock, so that a concurrent close either cancels this or
|
|
1567
|
+
# finds the new connection in place and closes it.
|
|
1568
|
+
synchronize do
|
|
1569
|
+
if @close_generation != generation
|
|
1570
|
+
@io.close
|
|
1571
|
+
@io = nil
|
|
1572
|
+
return
|
|
1573
|
+
end
|
|
1544
1574
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
@
|
|
1548
|
-
|
|
1575
|
+
# Clear pending flush calls and reset state before restarting loops
|
|
1576
|
+
@flush_queue.clear
|
|
1577
|
+
@pings_outstanding = 0
|
|
1578
|
+
@pongs_received = 0
|
|
1549
1579
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
@io.write(cmds.join) unless cmds.empty?
|
|
1555
|
-
@status = CONNECTED
|
|
1556
|
-
@pending_size = 0
|
|
1580
|
+
# Replay all subscriptions
|
|
1581
|
+
@subs.each_pair do |sid, sub|
|
|
1582
|
+
@io.write("SUB #{sub.subject} #{sub.queue} #{sid}#{CR_LF}")
|
|
1583
|
+
end
|
|
1557
1584
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1585
|
+
# Flush anything which was left pending, in case of errors during flush
|
|
1586
|
+
# then we should raise error then retry the reconnect logic
|
|
1587
|
+
cmds = []
|
|
1588
|
+
cmds << @pending_queue.pop until @pending_queue.empty?
|
|
1589
|
+
@io.write(cmds.join) unless cmds.empty?
|
|
1590
|
+
@status = CONNECTED
|
|
1591
|
+
@pending_size = 0
|
|
1561
1592
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1593
|
+
# Reset parser state here to avoid unknown protocol errors
|
|
1594
|
+
# on reconnect...
|
|
1595
|
+
@parser.reset!
|
|
1596
|
+
|
|
1597
|
+
# Now connected to NATS, and we can restart parser loop, flusher
|
|
1598
|
+
# and ping interval
|
|
1599
|
+
start_threads!
|
|
1600
|
+
end
|
|
1565
1601
|
|
|
1566
|
-
# Dispatch the reconnected callback while holding lock
|
|
1567
|
-
# which we should have already
|
|
1568
1602
|
@reconnect_cb&.call
|
|
1569
1603
|
end
|
|
1570
1604
|
|
|
1605
|
+
def closed_since?(generation)
|
|
1606
|
+
synchronize { @close_generation != generation }
|
|
1607
|
+
end
|
|
1608
|
+
|
|
1571
1609
|
def close_connection(conn_status, do_cbs = true)
|
|
1572
1610
|
synchronize do
|
|
1573
1611
|
@connect_called = false
|
|
@@ -1575,25 +1613,10 @@ module NATS
|
|
|
1575
1613
|
@status = conn_status
|
|
1576
1614
|
return
|
|
1577
1615
|
end
|
|
1616
|
+
@close_generation += 1
|
|
1578
1617
|
end
|
|
1579
1618
|
|
|
1580
|
-
|
|
1581
|
-
@flush_queue << :fallout if @flush_queue
|
|
1582
|
-
Thread.pass
|
|
1583
|
-
|
|
1584
|
-
# FIXME: More graceful way of handling the following?
|
|
1585
|
-
# Ensure ping interval and flusher are not running anymore
|
|
1586
|
-
if @ping_interval_thread&.alive?
|
|
1587
|
-
@ping_interval_thread.exit
|
|
1588
|
-
end
|
|
1589
|
-
|
|
1590
|
-
if @flusher_thread&.alive?
|
|
1591
|
-
@flusher_thread.exit
|
|
1592
|
-
end
|
|
1593
|
-
|
|
1594
|
-
if @read_loop_thread&.alive?
|
|
1595
|
-
@read_loop_thread.exit
|
|
1596
|
-
end
|
|
1619
|
+
stop_threads!
|
|
1597
1620
|
|
|
1598
1621
|
@subscription_executor&.shutdown
|
|
1599
1622
|
@subscription_executor&.wait_for_termination(options[:close_timeout])
|
|
@@ -1642,21 +1665,58 @@ module NATS
|
|
|
1642
1665
|
end
|
|
1643
1666
|
end
|
|
1644
1667
|
|
|
1668
|
+
# Asks the read loop, flusher and ping threads to stop, wakes them from
|
|
1669
|
+
# wherever they block, and waits for them to finish.
|
|
1670
|
+
#
|
|
1671
|
+
# They are never killed: Thread#exit on a thread that is waiting for
|
|
1672
|
+
# the client lock can swallow the lock's wakeup, so that the next
|
|
1673
|
+
# thread waiting for the lock sleeps forever on an unlocked lock (#183).
|
|
1674
|
+
def stop_threads!(timeout = NATS::IO::THREADS_STOP_TIMEOUT)
|
|
1675
|
+
threads = [@read_loop_thread, @flusher_thread, @ping_interval_thread].compact
|
|
1676
|
+
threads.each { |t| t[:nats_stop]&.stop! } # also wakes the ping thread
|
|
1677
|
+
|
|
1678
|
+
begin
|
|
1679
|
+
@flush_queue&.push(:fallout, true)
|
|
1680
|
+
rescue ThreadError
|
|
1681
|
+
# Queue is full, so the flusher is awake already.
|
|
1682
|
+
end
|
|
1683
|
+
# Wake the read loop if it is blocked on the socket. Only while it
|
|
1684
|
+
# runs: shutdown acts on the socket itself, which a forked child
|
|
1685
|
+
# (whose threads are gone) shares with the parent.
|
|
1686
|
+
if @read_loop_thread&.alive? && @read_loop_thread != Thread.current
|
|
1687
|
+
@io&.shutdown_read
|
|
1688
|
+
end
|
|
1689
|
+
|
|
1690
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
1691
|
+
threads.each do |t|
|
|
1692
|
+
next if t == Thread.current
|
|
1693
|
+
|
|
1694
|
+
t.join([deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max)
|
|
1695
|
+
end
|
|
1696
|
+
end
|
|
1697
|
+
|
|
1698
|
+
def stopping?
|
|
1699
|
+
Thread.current[:nats_stop]&.stopped?
|
|
1700
|
+
end
|
|
1701
|
+
|
|
1702
|
+
def start_thread(name, stop, &block)
|
|
1703
|
+
thread = Thread.new(&block)
|
|
1704
|
+
thread[:nats_stop] = stop
|
|
1705
|
+
thread.name = name
|
|
1706
|
+
thread.abort_on_exception = true
|
|
1707
|
+
thread
|
|
1708
|
+
end
|
|
1709
|
+
|
|
1645
1710
|
def start_threads!
|
|
1646
1711
|
# Reading loop for gathering data
|
|
1647
|
-
@read_loop_thread =
|
|
1648
|
-
@read_loop_thread.name = "nats:read_loop"
|
|
1649
|
-
@read_loop_thread.abort_on_exception = true
|
|
1712
|
+
@read_loop_thread = start_thread("nats:read_loop", NATS::IO::StopSignal.new) { read_loop }
|
|
1650
1713
|
|
|
1651
1714
|
# Flusher loop for sending commands
|
|
1652
|
-
@flusher_thread =
|
|
1653
|
-
@flusher_thread.name = "nats:flusher_loop"
|
|
1654
|
-
@flusher_thread.abort_on_exception = true
|
|
1715
|
+
@flusher_thread = start_thread("nats:flusher_loop", NATS::IO::StopSignal.new) { flusher_loop }
|
|
1655
1716
|
|
|
1656
1717
|
# Ping interval handling for keeping alive the connection
|
|
1657
|
-
|
|
1658
|
-
@ping_interval_thread
|
|
1659
|
-
@ping_interval_thread.abort_on_exception = true
|
|
1718
|
+
ping_stop = NATS::IO::StopSignal.new
|
|
1719
|
+
@ping_interval_thread = start_thread("nats:ping_loop", ping_stop) { ping_interval_loop(ping_stop) }
|
|
1660
1720
|
|
|
1661
1721
|
# Subscription handling thread pool
|
|
1662
1722
|
@subscription_executor = Concurrent::ThreadPoolExecutor.new(
|
|
@@ -1889,6 +1949,44 @@ module NATS
|
|
|
1889
1949
|
DEFAULT_DRAIN_TIMEOUT = 30
|
|
1890
1950
|
DEFAULT_CLOSE_TIMEOUT = 30
|
|
1891
1951
|
|
|
1952
|
+
# How long to wait for the read loop, flusher and ping threads to
|
|
1953
|
+
# stop on close or reconnect.
|
|
1954
|
+
THREADS_STOP_TIMEOUT = 5
|
|
1955
|
+
|
|
1956
|
+
# Asks a background thread to stop, and lets it sleep until then.
|
|
1957
|
+
class StopSignal
|
|
1958
|
+
def initialize
|
|
1959
|
+
@mutex = Mutex.new
|
|
1960
|
+
@cond = ConditionVariable.new
|
|
1961
|
+
@stopped = false
|
|
1962
|
+
end
|
|
1963
|
+
|
|
1964
|
+
def stop!
|
|
1965
|
+
@mutex.synchronize do
|
|
1966
|
+
@stopped = true
|
|
1967
|
+
@cond.broadcast
|
|
1968
|
+
end
|
|
1969
|
+
end
|
|
1970
|
+
|
|
1971
|
+
def stopped?
|
|
1972
|
+
@stopped
|
|
1973
|
+
end
|
|
1974
|
+
|
|
1975
|
+
# Sleeps up to timeout seconds; returns whether stop! was called.
|
|
1976
|
+
def wait(timeout)
|
|
1977
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
1978
|
+
@mutex.synchronize do
|
|
1979
|
+
until @stopped
|
|
1980
|
+
left = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1981
|
+
break if left <= 0
|
|
1982
|
+
|
|
1983
|
+
@cond.wait(@mutex, left)
|
|
1984
|
+
end
|
|
1985
|
+
@stopped
|
|
1986
|
+
end
|
|
1987
|
+
end
|
|
1988
|
+
end
|
|
1989
|
+
|
|
1892
1990
|
# Default Pending Limits
|
|
1893
1991
|
DEFAULT_SUB_PENDING_MSGS_LIMIT = 65536
|
|
1894
1992
|
DEFAULT_SUB_PENDING_BYTES_LIMIT = 65536 * 1024
|
|
@@ -2004,6 +2102,15 @@ module NATS
|
|
|
2004
2102
|
@socket.close
|
|
2005
2103
|
end
|
|
2006
2104
|
|
|
2105
|
+
# Makes a read blocked on the socket in another thread return,
|
|
2106
|
+
# while writes keep working.
|
|
2107
|
+
def shutdown_read
|
|
2108
|
+
io = @socket.respond_to?(:to_io) ? @socket.to_io : @socket
|
|
2109
|
+
io.shutdown(::Socket::SHUT_RD)
|
|
2110
|
+
rescue IOError, SystemCallError
|
|
2111
|
+
# Already closed or not connected.
|
|
2112
|
+
end
|
|
2113
|
+
|
|
2007
2114
|
def closed?
|
|
2008
2115
|
@socket.closed?
|
|
2009
2116
|
end
|
|
@@ -2030,6 +2137,12 @@ module NATS
|
|
|
2030
2137
|
end
|
|
2031
2138
|
|
|
2032
2139
|
sock
|
|
2140
|
+
rescue IOError => e
|
|
2141
|
+
# JRuby raises a plain IOError for a refused connection.
|
|
2142
|
+
if (RUBY_ENGINE == "jruby") && e.message.include?("Connection refused")
|
|
2143
|
+
raise Errno::ECONNREFUSED
|
|
2144
|
+
end
|
|
2145
|
+
raise
|
|
2033
2146
|
end
|
|
2034
2147
|
end
|
|
2035
2148
|
end
|
|
@@ -231,7 +231,7 @@ module NATS
|
|
|
231
231
|
|
|
232
232
|
data = req.to_json
|
|
233
233
|
if params[:direct]
|
|
234
|
-
if params[:subject] && !
|
|
234
|
+
if params[:subject] && !params[:seq]
|
|
235
235
|
# last_by_subject type request requires no payload.
|
|
236
236
|
data = ""
|
|
237
237
|
req_subject = "#{@prefix}.DIRECT.GET.#{stream_name}.#{params[:subject]}"
|
|
@@ -43,15 +43,19 @@ module NATS
|
|
|
43
43
|
Timestamp = 9
|
|
44
44
|
NumPending = 10
|
|
45
45
|
|
|
46
|
+
# Positions of fields that must be unsigned integers (ADR-15).
|
|
47
|
+
NumericTokens = [NumDelivered, StreamSeq, ConsumerSeq, Timestamp, NumPending].freeze
|
|
48
|
+
Digits = /\A\d+\z/
|
|
49
|
+
|
|
46
50
|
# Subject without domain:
|
|
47
51
|
# $JS.ACK.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending>
|
|
48
52
|
#
|
|
49
53
|
V1TokenCounts = 9
|
|
50
54
|
|
|
51
|
-
# Subject with domain:
|
|
52
|
-
# $JS.ACK.<domain>.<account hash>.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending
|
|
55
|
+
# Subject with domain, 11 or more tokens; the server may append more (ADR-15):
|
|
56
|
+
# $JS.ACK.<domain>.<account hash>.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending>
|
|
53
57
|
#
|
|
54
|
-
V2TokenCounts =
|
|
58
|
+
V2TokenCounts = 11
|
|
55
59
|
|
|
56
60
|
SequencePair = Struct.new(:stream, :consumer)
|
|
57
61
|
|
|
@@ -95,9 +95,9 @@ module NATS
|
|
|
95
95
|
n = tokens.count
|
|
96
96
|
|
|
97
97
|
if n < Ack::V1TokenCounts || ((n > Ack::V1TokenCounts) && (n < Ack::V2TokenCounts))
|
|
98
|
-
raise NotJSMessage.new("nats: not a jetstream message")
|
|
98
|
+
raise JetStream::Error::NotJSMessage.new("nats: not a jetstream message")
|
|
99
99
|
elsif tokens[0] != Ack::Prefix0 || tokens[1] != Ack::Prefix1
|
|
100
|
-
raise NotJSMessage.new("nats: not a jetstream message")
|
|
100
|
+
raise JetStream::Error::NotJSMessage.new("nats: not a jetstream message")
|
|
101
101
|
elsif n == Ack::V1TokenCounts
|
|
102
102
|
tokens.insert(Ack::Domain, Ack::Empty)
|
|
103
103
|
tokens.insert(Ack::AccHash, Ack::Empty)
|
|
@@ -105,6 +105,10 @@ module NATS
|
|
|
105
105
|
tokens[Ack::Domain] = Ack::Empty
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
+
unless Ack::NumericTokens.all? { |i| Ack::Digits.match?(tokens[i]) }
|
|
109
|
+
raise JetStream::Error::NotJSMessage.new("nats: not a jetstream message")
|
|
110
|
+
end
|
|
111
|
+
|
|
108
112
|
Metadata.new(tokens)
|
|
109
113
|
end
|
|
110
114
|
end
|
|
@@ -27,7 +27,8 @@ module NATS
|
|
|
27
27
|
@domain = opts[Ack::Domain]
|
|
28
28
|
@num_delivered = opts[Ack::NumDelivered].to_i
|
|
29
29
|
@num_pending = opts[Ack::NumPending].to_i
|
|
30
|
-
|
|
30
|
+
nanos = opts[Ack::Timestamp].to_i
|
|
31
|
+
@timestamp = Time.at(nanos / 1_000_000_000, nanos % 1_000_000_000, :nsec)
|
|
31
32
|
@stream = opts[Ack::Stream]
|
|
32
33
|
@consumer = opts[Ack::Consumer]
|
|
33
34
|
# TODO: Not exposed in Go client either right now.
|
data/lib/nats/io/jetstream.rb
CHANGED
|
@@ -305,7 +305,7 @@ module NATS
|
|
|
305
305
|
# If attempting to bind, then this is a hard error.
|
|
306
306
|
raise e if params[:stream] && !multi_filter
|
|
307
307
|
|
|
308
|
-
config = if !
|
|
308
|
+
config = if !params[:config]
|
|
309
309
|
JetStream::API::ConsumerConfig.new
|
|
310
310
|
elsif params[:config].is_a?(JetStream::API::ConsumerConfig)
|
|
311
311
|
params[:config]
|
data/lib/nats/io/kv.rb
CHANGED
|
@@ -31,7 +31,7 @@ module NATS
|
|
|
31
31
|
ROLLUP = "Nats-Rollup"
|
|
32
32
|
|
|
33
33
|
VALID_BUCKET_RE = /\A[a-zA-Z0-9_-]+$/
|
|
34
|
-
VALID_KEY_RE = /\A[-\/_
|
|
34
|
+
VALID_KEY_RE = /\A[-\/_=.a-zA-Z0-9]+$/
|
|
35
35
|
|
|
36
36
|
class << self
|
|
37
37
|
def is_valid_key(key)
|
|
@@ -273,7 +273,7 @@ module NATS
|
|
|
273
273
|
nc = @js.nc
|
|
274
274
|
watcher = KeyWatcher.new(@js)
|
|
275
275
|
|
|
276
|
-
deliver_policy = if !
|
|
276
|
+
deliver_policy = if !params[:include_history]
|
|
277
277
|
"last_per_subject"
|
|
278
278
|
end
|
|
279
279
|
|
|
@@ -327,15 +327,9 @@ module NATS
|
|
|
327
327
|
meta = msg.metadata
|
|
328
328
|
watcher.synchronize { watcher._active = true }
|
|
329
329
|
# Track the sequences
|
|
330
|
-
#
|
|
331
|
-
# $JS.ACK.KV_TEST.CKRGrWpf.1.10.10.1739859923871837000.0
|
|
332
|
-
#
|
|
333
|
-
tokens = msg.reply.split(".")
|
|
334
|
-
sseq = tokens[5]
|
|
335
|
-
dseq = tokens[6]
|
|
336
330
|
watcher.synchronize do
|
|
337
|
-
watcher._dseq =
|
|
338
|
-
watcher._sseq =
|
|
331
|
+
watcher._dseq = meta.sequence.consumer + 1
|
|
332
|
+
watcher._sseq = meta.sequence.stream
|
|
339
333
|
end
|
|
340
334
|
|
|
341
335
|
# Keys() handling
|
|
@@ -465,6 +459,7 @@ module NATS
|
|
|
465
459
|
class KeyWatcher
|
|
466
460
|
include MonitorMixin
|
|
467
461
|
include Enumerable
|
|
462
|
+
|
|
468
463
|
attr_accessor :received, :pending, :_sub, :_updates, :_init_done, :_watcher_cond
|
|
469
464
|
attr_accessor :_sseq, :_dseq, :_active, :_hb_task
|
|
470
465
|
|
data/lib/nats/io/parser.rb
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
module NATS
|
|
18
18
|
module Protocol
|
|
19
19
|
MSG = /\AMSG\s+([^\s]+)\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?(\d+)\r\n/i
|
|
20
|
-
HMSG = /\AHMSG\s+([^\s]+)\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?(
|
|
20
|
+
HMSG = /\AHMSG\s+([^\s]+)\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?(\d+)\s+(\d+)\r\n/i
|
|
21
21
|
OK = /\A\+OK\s*\r\n/i
|
|
22
22
|
ERR = /\A-ERR\s+('.+')?\r\n/i
|
|
23
23
|
PING = /\APING\s*\r\n/i
|
|
@@ -98,12 +98,12 @@ module NATS
|
|
|
98
98
|
return unless @needed && @buf.bytesize >= (@needed + CR_LF_SIZE)
|
|
99
99
|
if @header_needed
|
|
100
100
|
hbuf = @buf.slice(0, @header_needed)
|
|
101
|
-
payload = @buf.slice(@header_needed,
|
|
101
|
+
payload = @buf.slice(@header_needed, @needed - @header_needed)
|
|
102
102
|
@nc.send(:process_msg, @sub, @sid, @reply, payload, hbuf)
|
|
103
103
|
else
|
|
104
104
|
@nc.send(:process_msg, @sub, @sid, @reply, @buf.slice(0, @needed), nil)
|
|
105
105
|
end
|
|
106
|
-
@buf = @buf.slice(
|
|
106
|
+
@buf = @buf.slice(@needed + CR_LF_SIZE, @buf.bytesize)
|
|
107
107
|
|
|
108
108
|
@sub = @sid = @reply = @needed = @header_needed = nil
|
|
109
109
|
@parse_state = AWAITING_CONTROL_LINE
|
data/lib/nats/io/subscription.rb
CHANGED
|
@@ -148,13 +148,20 @@ module NATS
|
|
|
148
148
|
msg = pending_queue.pop(true)
|
|
149
149
|
process(msg)
|
|
150
150
|
rescue ThreadError # queue is empty
|
|
151
|
-
|
|
151
|
+
# No release here: the ensure below releases the permit; a second
|
|
152
|
+
# release would grow the semaphore beyond processing_concurrency.
|
|
152
153
|
ensure
|
|
153
154
|
concurrency_semaphore.release
|
|
154
155
|
[concurrency_semaphore.available_permits, pending_queue.size].min.times do
|
|
155
156
|
enqueue_processing(executor)
|
|
156
157
|
end
|
|
157
158
|
end
|
|
159
|
+
rescue Concurrent::RejectedExecutionError
|
|
160
|
+
# The executor is being shut down (the connection is closing,
|
|
161
|
+
# draining or reconnecting). Release the permit so the
|
|
162
|
+
# subscription can process messages again after a reconnect;
|
|
163
|
+
# the message stays in pending_queue.
|
|
164
|
+
concurrency_semaphore.release
|
|
158
165
|
end
|
|
159
166
|
end
|
|
160
167
|
end
|
data/lib/nats/io/version.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Copyright 2016-
|
|
3
|
+
# Copyright 2016-2026 The NATS Authors
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
5
|
# you may not use this file except in compliance with the License.
|
|
6
6
|
# You may obtain a copy of the License at
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
module NATS
|
|
18
18
|
module IO
|
|
19
19
|
# VERSION is the version of the client announced on CONNECT to the server.
|
|
20
|
-
VERSION = "2.
|
|
20
|
+
VERSION = "2.6.0"
|
|
21
21
|
|
|
22
22
|
# LANG is the lang runtime of the client announced on CONNECT to the server.
|
|
23
23
|
LANG = "#{RUBY_ENGINE}#{RUBY_VERSION}".freeze
|
data/lib/nats/io/websocket.rb
CHANGED
|
@@ -47,23 +47,28 @@ module NATS
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def read(max_bytes = MAX_SOCKET_READ_BYTES, deadline = nil)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
result << msg
|
|
50
|
+
# Hand over what read_line decoded beyond its last line first,
|
|
51
|
+
# otherwise those bytes are lost once the read loop takes over.
|
|
52
|
+
if @line_buf && !@line_buf.empty?
|
|
53
|
+
return @line_buf.slice!(0, @line_buf.bytesize)
|
|
55
54
|
end
|
|
56
|
-
|
|
55
|
+
|
|
56
|
+
read_frames(max_bytes, deadline)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def read_line(deadline = nil)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
@line_buf ||= +""
|
|
61
|
+
loop do
|
|
62
|
+
if (idx = @line_buf =~ /\r?\n/)
|
|
63
|
+
return @line_buf.slice!(0, idx + Regexp.last_match(0).length)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Pull more data from the wire; never from @line_buf itself,
|
|
67
|
+
# or a partial line would be fed back forever.
|
|
68
|
+
data = read_frames(MAX_SOCKET_READ_BYTES, deadline)
|
|
69
|
+
return nil unless data
|
|
70
|
+
@line_buf << data
|
|
65
71
|
end
|
|
66
|
-
result.join
|
|
67
72
|
end
|
|
68
73
|
|
|
69
74
|
def write(data, deadline = nil)
|
|
@@ -72,6 +77,21 @@ module NATS
|
|
|
72
77
|
frame = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: :binary, version: @handshake.version)
|
|
73
78
|
super(frame.to_s)
|
|
74
79
|
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Reads from the socket and returns the payloads of all complete
|
|
84
|
+
# frames decoded so far.
|
|
85
|
+
def read_frames(max_bytes, deadline)
|
|
86
|
+
data = Socket.instance_method(:read).bind_call(self, max_bytes, deadline)
|
|
87
|
+
@frame << data
|
|
88
|
+
[].tap do |parts|
|
|
89
|
+
while (msg = @frame.next)
|
|
90
|
+
payload = msg.respond_to?(:data) ? msg.data : msg
|
|
91
|
+
parts << payload if payload
|
|
92
|
+
end
|
|
93
|
+
end.join
|
|
94
|
+
end
|
|
75
95
|
end
|
|
76
96
|
end
|
|
77
97
|
end
|
|
@@ -6,8 +6,8 @@ module NATS
|
|
|
6
6
|
REGEX = {
|
|
7
7
|
name: /[A-Za-z0-9\-_]+$/,
|
|
8
8
|
version: /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/,
|
|
9
|
-
subject: /^[^ >]
|
|
10
|
-
queue: /^[^ >]
|
|
9
|
+
subject: /^[^ >]*>?$/,
|
|
10
|
+
queue: /^[^ >]*>?$/
|
|
11
11
|
}.freeze
|
|
12
12
|
|
|
13
13
|
class << self
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nats-pure
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Waldemar Quevedo
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: concurrent-ruby
|
|
@@ -267,7 +266,6 @@ metadata:
|
|
|
267
266
|
documentation_uri: https://github.com/nats-io/nats-pure.rb
|
|
268
267
|
homepage_uri: https://github.com/nats-io/nats-pure.rb
|
|
269
268
|
source_code_uri: https://github.com/nats-io/nats-pure.rb
|
|
270
|
-
post_install_message:
|
|
271
269
|
rdoc_options: []
|
|
272
270
|
require_paths:
|
|
273
271
|
- lib
|
|
@@ -282,8 +280,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
282
280
|
- !ruby/object:Gem::Version
|
|
283
281
|
version: '0'
|
|
284
282
|
requirements: []
|
|
285
|
-
rubygems_version:
|
|
286
|
-
signing_key:
|
|
283
|
+
rubygems_version: 4.0.21
|
|
287
284
|
specification_version: 4
|
|
288
285
|
summary: NATS is an open-source, high-performance, lightweight cloud messaging system.
|
|
289
286
|
test_files: []
|