serverish 2.1.0__tar.gz → 2.2.0__tar.gz
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.
- {serverish-2.1.0 → serverish-2.2.0}/PKG-INFO +2 -2
- {serverish-2.1.0 → serverish-2.2.0}/pyproject.toml +5 -5
- {serverish-2.1.0 → serverish-2.2.0}/serverish/base/exceptions.py +31 -8
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_publisher.py +45 -2
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_reader.py +83 -63
- {serverish-2.1.0 → serverish-2.2.0}/LICENSE +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/README.md +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/__init__.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/base/__init__.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/base/asyncio_util_functions.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/base/collector.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/base/datetime.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/base/fifoset.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/base/hasstatuses.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/base/idmanger.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/base/iterators.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/base/manageable.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/base/singleton.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/base/status.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/base/task_manager.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/connection/__init__.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/connection/connection.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/connection/connection_jets.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/connection/connection_nats.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/__init__.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/live_document.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/logging.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/messenger.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_callback_sub.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_cmd_pub.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_cmd_sub.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_core_pub.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_core_read.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_core_sub.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_document_read.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_journal_pub.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_journal_read.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_kv.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_kv_read.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_kv_store.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_kv_sub.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_progress_pub.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_progress_read.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_rpc_req.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_rpc_resp.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_single_pub.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msg_single_read.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/messenger/msgvalidator.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/monitoring/__init__.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/monitoring/bridge.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/monitoring/factory.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/monitoring/monitored.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/monitoring/monitored_nats.py +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/schema/config.schema.json +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/schema/default.schema.json +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/schema/include/timestamp.schema.json +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/schema/journal.schema.json +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/schema/meta.schema.json +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/schema/program_current_state.schema.json +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/schema/program_state.schema.json +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/schema/progress.schema.json +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/schema/telemetry.schema.json +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/services/TODO.txt +0 -0
- {serverish-2.1.0 → serverish-2.2.0}/serverish/slog.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: serverish
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: helpers for server alike projects
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: Mikołaj Kałuszyński
|
|
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
14
14
|
Provides-Extra: dns
|
|
15
15
|
Provides-Extra: messenger
|
|
16
|
-
Requires-Dist: aiodns (>=3.2
|
|
16
|
+
Requires-Dist: aiodns (>=3.2,<5) ; extra == "dns"
|
|
17
17
|
Requires-Dist: jsonschema (>=4.20.0,<5.0.0)
|
|
18
18
|
Requires-Dist: nats-py (>=2.7.2,<3.0.0) ; extra == "messenger"
|
|
19
19
|
Requires-Dist: param (>=2.1.0,<3.0.0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "serverish"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.2.0"
|
|
4
4
|
description = "helpers for server alike projects"
|
|
5
5
|
authors = ["Mikołaj Kałuszyński", "MMME team"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -9,7 +9,7 @@ include = ["serverish/schema/*"]
|
|
|
9
9
|
|
|
10
10
|
[tool.poetry.dependencies]
|
|
11
11
|
python = "^3.10"
|
|
12
|
-
aiodns = {version="
|
|
12
|
+
aiodns = {version=">=3.2,<5", optional=true}
|
|
13
13
|
param = "^2.1.0"
|
|
14
14
|
nats-py = { version = "^2.7.2", optional= true}
|
|
15
15
|
jsonschema = "^4.20.0"
|
|
@@ -20,12 +20,12 @@ dns = ["aiodns"]
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
[tool.poetry.group.dev.dependencies]
|
|
23
|
-
pytest = ">=
|
|
24
|
-
pytest-asyncio = "
|
|
23
|
+
pytest = ">=9.1,<10"
|
|
24
|
+
pytest-asyncio = "^1.4"
|
|
25
25
|
nats-py = "^2.6.0"
|
|
26
26
|
jupyter = "*"
|
|
27
27
|
rich = "^13.5.2"
|
|
28
|
-
aiodns = "
|
|
28
|
+
aiodns = ">=3.1.1,<5"
|
|
29
29
|
testcontainers = {extras = ["nats"], version = "^4.14.2"}
|
|
30
30
|
pytest-timeout = "^2.4.0"
|
|
31
31
|
|
|
@@ -1,13 +1,24 @@
|
|
|
1
|
-
class
|
|
1
|
+
class ServerishError(Exception):
|
|
2
|
+
"""Common base for all serverish exceptions
|
|
3
|
+
|
|
4
|
+
Catch this to handle any error originating from the serverish library,
|
|
5
|
+
regardless of the subsystem (messenger, connection, KV). Concrete
|
|
6
|
+
exceptions additionally inherit fitting standard bases (ValueError,
|
|
7
|
+
TimeoutError, KeyError...), so idiomatic generic handlers keep working.
|
|
8
|
+
"""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MessengerCannotConnect(ServerishError):
|
|
2
13
|
pass
|
|
3
14
|
|
|
4
|
-
class MessengerNotConnected(
|
|
15
|
+
class MessengerNotConnected(ServerishError):
|
|
5
16
|
pass
|
|
6
17
|
|
|
7
|
-
class MessengerReaderStopped(
|
|
18
|
+
class MessengerReaderStopped(ServerishError):
|
|
8
19
|
pass
|
|
9
20
|
|
|
10
|
-
class MessengerReaderConfigError(ValueError):
|
|
21
|
+
class MessengerReaderConfigError(ServerishError, ValueError):
|
|
11
22
|
"""Raised when a MsgReader is misconfigured (e.g. missing required start marker).
|
|
12
23
|
|
|
13
24
|
This is a fatal error — the same configuration will never succeed against
|
|
@@ -16,10 +27,10 @@ class MessengerReaderConfigError(ValueError):
|
|
|
16
27
|
"""
|
|
17
28
|
pass
|
|
18
29
|
|
|
19
|
-
class MessengerReaderAlreadyOpen(RuntimeError):
|
|
30
|
+
class MessengerReaderAlreadyOpen(ServerishError, RuntimeError):
|
|
20
31
|
pass
|
|
21
32
|
|
|
22
|
-
class MessengerRequestNoResponse(
|
|
33
|
+
class MessengerRequestNoResponse(ServerishError):
|
|
23
34
|
pass
|
|
24
35
|
|
|
25
36
|
class MessengerRequestNoResponders(MessengerRequestNoResponse):
|
|
@@ -41,7 +52,19 @@ class MessengerRequestNoResultYet(MessengerRequestNoResponse):
|
|
|
41
52
|
pass
|
|
42
53
|
|
|
43
54
|
|
|
44
|
-
class
|
|
55
|
+
class MessengerPublishAckTimeout(ServerishError, TimeoutError):
|
|
56
|
+
"""Raised when JetStream did not confirm a publish within the ack timeout.
|
|
57
|
+
|
|
58
|
+
The message MAY have been delivered and stored — only the acknowledgement
|
|
59
|
+
did not arrive (or was not processed) in time. A starved client event loop
|
|
60
|
+
is the most common cause, not an actual delivery failure. Publishes carry
|
|
61
|
+
a ``Nats-Msg-Id`` header, so JetStream deduplicates retries of the same
|
|
62
|
+
message within the stream's duplicate window.
|
|
63
|
+
"""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class MessengerKvError(ServerishError):
|
|
45
68
|
pass
|
|
46
69
|
|
|
47
70
|
|
|
@@ -61,4 +84,4 @@ class MessengerKvMalformed(MessengerKvError):
|
|
|
61
84
|
An entry written by a non-serverish client fails loudly instead of being
|
|
62
85
|
silently returned as empty.
|
|
63
86
|
"""
|
|
64
|
-
pass
|
|
87
|
+
pass
|
|
@@ -7,7 +7,7 @@ import nats.errors
|
|
|
7
7
|
import nats.js
|
|
8
8
|
import param
|
|
9
9
|
|
|
10
|
-
from serverish.base import MessengerNotConnected
|
|
10
|
+
from serverish.base import MessengerNotConnected, MessengerPublishAckTimeout
|
|
11
11
|
from serverish.messenger import Messenger
|
|
12
12
|
from serverish.messenger.messenger import MsgDriver, log
|
|
13
13
|
|
|
@@ -18,8 +18,19 @@ class MsgPublisher(MsgDriver):
|
|
|
18
18
|
Use this class if you want to publish data to a messenger subject.
|
|
19
19
|
Check for specialist publishers for common use cases.
|
|
20
20
|
|
|
21
|
+
Every publish carries a ``Nats-Msg-Id`` header equal to the message
|
|
22
|
+
``meta.id``, so JetStream deduplicates repeated deliveries of the same
|
|
23
|
+
message within the stream's duplicate window. This makes retrying after
|
|
24
|
+
an ack timeout safe: if the original publish did reach the stream (a
|
|
25
|
+
common case when the client event loop is starved and the ack is simply
|
|
26
|
+
processed too late), the retry is discarded by the server.
|
|
27
|
+
|
|
21
28
|
Parameters:
|
|
22
29
|
raise_on_publish_error (bool): Raise on publish error, default `True` re-raises underlying exceptions
|
|
30
|
+
ack_timeout_retries (int): How many times to retry a publish for which no JetStream
|
|
31
|
+
ack arrived in time (default 2). Retries are deduplicated server-side via
|
|
32
|
+
``Nats-Msg-Id``. When retries are exhausted, `MessengerPublishAckTimeout` is
|
|
33
|
+
raised (subject to `raise_on_publish_error`).
|
|
23
34
|
|
|
24
35
|
Health monitoring:
|
|
25
36
|
The publisher tracks publish statistics accessible via `health_status` property:
|
|
@@ -30,6 +41,9 @@ class MsgPublisher(MsgDriver):
|
|
|
30
41
|
"""
|
|
31
42
|
|
|
32
43
|
raise_on_publish_error = param.Boolean(default=True, doc="Raise on publish error")
|
|
44
|
+
ack_timeout_retries = param.Integer(default=2, bounds=(0, None),
|
|
45
|
+
doc="Retries when JetStream ack does not arrive in time; "
|
|
46
|
+
"safe thanks to Nats-Msg-Id deduplication")
|
|
33
47
|
|
|
34
48
|
def __init__(self, **kwargs) -> None:
|
|
35
49
|
# Health monitoring fields
|
|
@@ -60,8 +74,10 @@ class MsgPublisher(MsgDriver):
|
|
|
60
74
|
log.error(f"Message {msg['meta']['id']} validation error: {e}")
|
|
61
75
|
raise e
|
|
62
76
|
self.messenger.log_msg_trace(msg['data'], msg['meta'], f"PUB to {self.subject}")
|
|
77
|
+
headers = dict(kwargs.pop('headers', None) or {})
|
|
78
|
+
headers.setdefault('Nats-Msg-Id', msg['meta']['id']) # server-side dedup, makes ack-timeout retries safe
|
|
63
79
|
try:
|
|
64
|
-
await self.
|
|
80
|
+
await self._publish_with_ack_retries(bdata, headers, msg['meta']['id'], **kwargs)
|
|
65
81
|
# Track successful publish
|
|
66
82
|
self._publish_count += 1
|
|
67
83
|
self._last_publish_time = time.monotonic()
|
|
@@ -90,6 +106,33 @@ class MsgPublisher(MsgDriver):
|
|
|
90
106
|
msg['meta']['status'] = str(e)
|
|
91
107
|
return msg
|
|
92
108
|
|
|
109
|
+
async def _publish_with_ack_retries(self, bdata: bytes, headers: dict, msg_id: str, **kwargs) -> None:
|
|
110
|
+
"""Publishes to JetStream, retrying when the ack does not arrive in time
|
|
111
|
+
|
|
112
|
+
A missing ack does not mean the message was not delivered - it may
|
|
113
|
+
well be stored while the ack was processed too late (e.g. starved
|
|
114
|
+
client event loop). Retries are deduplicated server-side via the
|
|
115
|
+
Nats-Msg-Id header, so this never produces duplicates.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
MessengerPublishAckTimeout: no ack after all retries
|
|
119
|
+
"""
|
|
120
|
+
for attempt in range(self.ack_timeout_retries + 1):
|
|
121
|
+
try:
|
|
122
|
+
await self.connection.js.publish(self.subject, bdata, headers=headers, **kwargs)
|
|
123
|
+
return
|
|
124
|
+
except nats.errors.TimeoutError as e:
|
|
125
|
+
if attempt < self.ack_timeout_retries:
|
|
126
|
+
log.warning(f"No JetStream ack for message {msg_id} on '{self.subject}' "
|
|
127
|
+
f"(attempt {attempt + 1}/{self.ack_timeout_retries + 1}), retrying - "
|
|
128
|
+
f"deduplicated by Nats-Msg-Id")
|
|
129
|
+
continue
|
|
130
|
+
raise MessengerPublishAckTimeout(
|
|
131
|
+
f"No JetStream ack for message {msg_id} on '{self.subject}' "
|
|
132
|
+
f"after {self.ack_timeout_retries + 1} attempts. The message MAY have been "
|
|
133
|
+
f"delivered - a missing ack often means a starved client event loop, "
|
|
134
|
+
f"not a delivery failure") from e
|
|
135
|
+
|
|
93
136
|
@property
|
|
94
137
|
def health_status(self) -> dict:
|
|
95
138
|
"""Returns current health status of the publisher for monitoring
|
|
@@ -86,6 +86,7 @@ class MsgReader(MsgDriver):
|
|
|
86
86
|
}
|
|
87
87
|
if consumer_cfg is not None:
|
|
88
88
|
consumer_cfg_defaults.update(consumer_cfg)
|
|
89
|
+
# pull request size == read-ahead buffer bound; becomes BatchPolicy (docs/design/POLICIES.md)
|
|
89
90
|
self.batch = 100
|
|
90
91
|
self.messages = deque()
|
|
91
92
|
self.pull_subscription: JetStreamContext.PullSubscription | None = None
|
|
@@ -435,12 +436,16 @@ class MsgReader(MsgDriver):
|
|
|
435
436
|
Uses no_wait=True protocol:
|
|
436
437
|
- Server sends available messages immediately
|
|
437
438
|
- Server sends 404 status if no messages exist
|
|
438
|
-
|
|
439
|
+
|
|
440
|
+
End-of-data is detected deterministically from the `num_pending`
|
|
441
|
+
counter the server stamps on every delivered message - the call
|
|
442
|
+
returns as soon as the server confirms there is nothing more (or the
|
|
443
|
+
batch is full), with no fixed waiting anywhere on the happy path.
|
|
439
444
|
|
|
440
445
|
Args:
|
|
441
446
|
batch: Maximum number of messages to fetch
|
|
442
|
-
timeout: Network
|
|
443
|
-
NOT a "wait for messages" timeout
|
|
447
|
+
timeout: Network-failure backstop (default 2s). Never consumed on
|
|
448
|
+
the happy path - NOT a "wait for messages" timeout
|
|
444
449
|
|
|
445
450
|
Returns:
|
|
446
451
|
List of messages that were immediately available on server
|
|
@@ -477,78 +482,93 @@ class MsgReader(MsgDriver):
|
|
|
477
482
|
pull_subscription._deliver,
|
|
478
483
|
)
|
|
479
484
|
|
|
480
|
-
# Wait for server response (messages and/or
|
|
481
|
-
#
|
|
482
|
-
#
|
|
485
|
+
# Wait for server response (messages and/or closing status).
|
|
486
|
+
#
|
|
487
|
+
# Deterministic end-of-data: every JetStream delivery is stamped with
|
|
488
|
+
# `num_pending` — the server-side count of messages still remaining
|
|
489
|
+
# for this consumer *after* that message (correct also for filtered
|
|
490
|
+
# and last_per_subject consumers). The loop therefore ends without
|
|
491
|
+
# any arbitrary grace period:
|
|
492
|
+
# - a message arrives with num_pending == 0 → nothing left, return,
|
|
493
|
+
# - batch filled while num_pending > 0 → return, the caller pulls again,
|
|
494
|
+
# - 404 (empty subject) / 408 (partial-batch closure) → server-confirmed end.
|
|
495
|
+
# `timeout` is a network-failure backstop only; no happy path consumes
|
|
496
|
+
# it, so the call is as fast as the server and the network allow.
|
|
483
497
|
start_time = time.monotonic()
|
|
484
|
-
got_404_status = False
|
|
485
|
-
got_any_message = False
|
|
486
|
-
|
|
487
|
-
while not got_404_status:
|
|
488
|
-
deadline = timeout - (time.monotonic() - start_time)
|
|
489
|
-
if deadline <= 0:
|
|
490
|
-
# Timeout - if we got some messages, that's OK (server might not send 404 after exact batch)
|
|
491
|
-
# If we got nothing, this is a network problem
|
|
492
|
-
if not got_any_message and len(msgs) == 0:
|
|
493
|
-
log.warning(f"{self} Timeout after {timeout}s waiting for server response (network issue?)")
|
|
494
|
-
else:
|
|
495
|
-
log.debug(f"{self} Timeout after getting {len(msgs)} messages (no 404 received)")
|
|
496
|
-
break
|
|
497
|
-
|
|
498
|
-
try:
|
|
499
|
-
# Wait for server to respond (messages or 404)
|
|
500
|
-
msg = await asyncio.wait_for(queue.get(), timeout=deadline)
|
|
501
|
-
pull_subscription._sub._pending_size -= len(msg.data)
|
|
502
|
-
queue.task_done()
|
|
503
498
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
got_404_status = True
|
|
511
|
-
break
|
|
512
|
-
elif status == "408":
|
|
513
|
-
# Request Timeout - server gave us what it had but couldn't fill batch
|
|
514
|
-
# This is effectively "no more messages available right now"
|
|
515
|
-
log.debug(f"{self} Server returned partial batch (408 status - request timeout)")
|
|
516
|
-
got_404_status = True # Treat same as 404 - no more messages available
|
|
517
|
-
break
|
|
518
|
-
elif status:
|
|
519
|
-
# Other status message (shouldn't happen with pull, but handle it)
|
|
520
|
-
log.debug(f"{self} Received status: {status}")
|
|
521
|
-
continue
|
|
522
|
-
else:
|
|
523
|
-
# Real message
|
|
524
|
-
msgs.append(msg)
|
|
525
|
-
got_any_message = True
|
|
526
|
-
needed -= 1
|
|
499
|
+
def log_backstop():
|
|
500
|
+
if msgs:
|
|
501
|
+
log.warning(f"{self} No end-of-data signal within {timeout}s backstop, "
|
|
502
|
+
f"returning {len(msgs)} messages (no num_pending metadata? network issue?)")
|
|
503
|
+
else:
|
|
504
|
+
log.warning(f"{self} Timeout after {timeout}s waiting for server response (network issue?)")
|
|
527
505
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
remaining_time = timeout - (time.monotonic() - start_time)
|
|
534
|
-
if remaining_time > 0.1:
|
|
535
|
-
deadline = 0.1
|
|
536
|
-
timeout = time.monotonic() - start_time + 0.1 # Update timeout to exit on next deadline check
|
|
506
|
+
while True:
|
|
507
|
+
remaining = timeout - (time.monotonic() - start_time)
|
|
508
|
+
if remaining <= 0:
|
|
509
|
+
log_backstop()
|
|
510
|
+
break
|
|
537
511
|
|
|
512
|
+
try:
|
|
513
|
+
msg = await asyncio.wait_for(queue.get(), timeout=remaining)
|
|
538
514
|
except asyncio.TimeoutError:
|
|
539
|
-
|
|
540
|
-
if got_any_message or len(msgs) > 0:
|
|
541
|
-
log.debug(f"{self} Got {len(msgs)} messages, no 404 status (normal for exact batch)")
|
|
542
|
-
else:
|
|
543
|
-
# No messages at all - this could be empty subject or network problem
|
|
544
|
-
log.debug(f"{self} No messages or 404 within {timeout}s")
|
|
515
|
+
log_backstop()
|
|
545
516
|
break
|
|
546
517
|
except Exception as e:
|
|
547
518
|
log.warning(f"{self} Error in fetch_available: {e}")
|
|
548
519
|
break
|
|
520
|
+
pull_subscription._sub._pending_size -= len(msg.data)
|
|
521
|
+
queue.task_done()
|
|
522
|
+
|
|
523
|
+
status = JetStreamContext.is_status_msg(msg)
|
|
524
|
+
if status == "404":
|
|
525
|
+
# Server says: no messages available at all
|
|
526
|
+
log.debug(f"{self} Server confirmed no messages (404 status)")
|
|
527
|
+
break
|
|
528
|
+
elif status == "408":
|
|
529
|
+
# Request Timeout - server closed the request after a partial batch
|
|
530
|
+
log.debug(f"{self} Server closed partial batch (408 status)")
|
|
531
|
+
break
|
|
532
|
+
elif status:
|
|
533
|
+
# Other status message (shouldn't happen with pull, but handle it)
|
|
534
|
+
log.debug(f"{self} Received status: {status}")
|
|
535
|
+
continue
|
|
536
|
+
|
|
537
|
+
# Real message
|
|
538
|
+
msgs.append(msg)
|
|
539
|
+
needed -= 1
|
|
540
|
+
|
|
541
|
+
num_pending = self._num_pending(msg)
|
|
542
|
+
if num_pending == 0:
|
|
543
|
+
# Server-confirmed end of data - no reason to wait for the
|
|
544
|
+
# closing status (which may trail by a network round trip,
|
|
545
|
+
# or never come for an exact-batch response)
|
|
546
|
+
log.debug(f"{self} Server reports num_pending=0, returning {len(msgs)} messages")
|
|
547
|
+
break
|
|
548
|
+
if needed == 0:
|
|
549
|
+
# Batch full and the server has more - return immediately,
|
|
550
|
+
# the caller decides whether to pull again
|
|
551
|
+
log.debug(f"{self} Batch of {len(msgs)} filled, "
|
|
552
|
+
f"server reports {num_pending} message(s) still pending")
|
|
553
|
+
break
|
|
554
|
+
# num_pending unknown (no JS metadata): keep reading until a
|
|
555
|
+
# closing status arrives or the backstop hits (degraded mode)
|
|
549
556
|
|
|
550
557
|
return msgs
|
|
551
558
|
|
|
559
|
+
@staticmethod
|
|
560
|
+
def _num_pending(msg: Msg) -> int | None:
|
|
561
|
+
"""Returns server-side `num_pending` from JetStream message metadata
|
|
562
|
+
|
|
563
|
+
`num_pending` is the number of messages still remaining for this
|
|
564
|
+
consumer after the given message. Returns None if the metadata is
|
|
565
|
+
unavailable (non-JetStream message).
|
|
566
|
+
"""
|
|
567
|
+
try:
|
|
568
|
+
return msg.metadata.num_pending
|
|
569
|
+
except Exception:
|
|
570
|
+
return None
|
|
571
|
+
|
|
552
572
|
async def next_msg(self, timeout: Optional[float] = None) -> Msg:
|
|
553
573
|
""" [NATS fix] Fetch the next message from the subscription
|
|
554
574
|
:params timeout: Time in seconds to wait for next message before timing out.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|