serverish 2.2.1__tar.gz → 2.3.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.2.1 → serverish-2.3.0}/PKG-INFO +5 -1
- {serverish-2.2.1 → serverish-2.3.0}/README.md +4 -0
- {serverish-2.2.1 → serverish-2.3.0}/pyproject.toml +2 -2
- {serverish-2.2.1 → serverish-2.3.0}/serverish/base/exceptions.py +52 -1
- serverish-2.3.0/serverish/connection/nats_errors.py +97 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/messenger.py +9 -4
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_callback_sub.py +6 -1
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_core_pub.py +14 -11
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_core_read.py +8 -3
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_kv.py +10 -8
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_kv_read.py +7 -4
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_kv_store.py +24 -16
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_publisher.py +24 -13
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_reader.py +32 -11
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_rpc_req.py +15 -11
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_rpc_resp.py +11 -6
- {serverish-2.2.1 → serverish-2.3.0}/LICENSE +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/__init__.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/base/__init__.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/base/asyncio_util_functions.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/base/collector.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/base/datetime.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/base/fifoset.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/base/hasstatuses.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/base/idmanger.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/base/iterators.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/base/manageable.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/base/singleton.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/base/status.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/base/task_manager.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/connection/__init__.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/connection/connection.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/connection/connection_jets.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/connection/connection_nats.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/__init__.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/live_document.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/logging.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_cmd_pub.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_cmd_sub.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_core_sub.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_document_read.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_journal_pub.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_journal_read.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_kv_sub.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_progress_pub.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_progress_read.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_single_pub.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msg_single_read.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/messenger/msgvalidator.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/monitoring/__init__.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/monitoring/bridge.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/monitoring/factory.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/monitoring/monitored.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/monitoring/monitored_nats.py +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/schema/config.schema.json +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/schema/default.schema.json +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/schema/include/timestamp.schema.json +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/schema/journal.schema.json +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/schema/meta.schema.json +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/schema/program_current_state.schema.json +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/schema/program_state.schema.json +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/schema/progress.schema.json +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/schema/telemetry.schema.json +0 -0
- {serverish-2.2.1 → serverish-2.3.0}/serverish/services/TODO.txt +0 -0
- {serverish-2.2.1 → serverish-2.3.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.3.0
|
|
4
4
|
Summary: helpers for server alike projects
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: Mikołaj Kałuszyński
|
|
@@ -50,6 +50,10 @@ this will install `nats-py` package.
|
|
|
50
50
|
- Custom JetStream API replacing nats-py private internals access
|
|
51
51
|
|
|
52
52
|
## Changes
|
|
53
|
+
* 2.3 **Breaking:** exceptions of the NATS client library (`nats.errors.*`) no longer leak from the
|
|
54
|
+
Messenger API - every error is a `ServerishError` (see [Error handling](doc/concepts.md#error-handling)).
|
|
55
|
+
Replace `except nats.errors...` handlers around serverish calls. `raise_on_publish_error=False`
|
|
56
|
+
covers a missing connection too.
|
|
53
57
|
* 1.7 Subscription reliability: reader auto-recovers from NATS disconnects and consumer expiry,
|
|
54
58
|
RPC responder auto-resubscribes, `health_status` property on all drivers (reader, publisher,
|
|
55
59
|
RPC responder, connection) with reconnect counts, error tracking, slow consumer detection.
|
|
@@ -28,6 +28,10 @@ this will install `nats-py` package.
|
|
|
28
28
|
- Custom JetStream API replacing nats-py private internals access
|
|
29
29
|
|
|
30
30
|
## Changes
|
|
31
|
+
* 2.3 **Breaking:** exceptions of the NATS client library (`nats.errors.*`) no longer leak from the
|
|
32
|
+
Messenger API - every error is a `ServerishError` (see [Error handling](doc/concepts.md#error-handling)).
|
|
33
|
+
Replace `except nats.errors...` handlers around serverish calls. `raise_on_publish_error=False`
|
|
34
|
+
covers a missing connection too.
|
|
31
35
|
* 1.7 Subscription reliability: reader auto-recovers from NATS disconnects and consumer expiry,
|
|
32
36
|
RPC responder auto-resubscribes, `health_status` property on all drivers (reader, publisher,
|
|
33
37
|
RPC responder, connection) with reconnect counts, error tracking, slow consumer detection.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "serverish"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.3.0"
|
|
4
4
|
description = "helpers for server alike projects"
|
|
5
5
|
authors = ["Mikołaj Kałuszyński", "MMME team"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -24,7 +24,7 @@ pytest = ">=9.1,<10"
|
|
|
24
24
|
pytest-asyncio = "^1.4"
|
|
25
25
|
nats-py = "^2.6.0"
|
|
26
26
|
jupyter = "*"
|
|
27
|
-
rich = "
|
|
27
|
+
rich = ">=13.5.2"
|
|
28
28
|
aiodns = ">=3.1.1,<5"
|
|
29
29
|
testcontainers = {extras = ["nats"], version = "^4.14.2"}
|
|
30
30
|
pytest-timeout = "^2.4.0"
|
|
@@ -13,6 +13,57 @@ class MessengerCannotConnect(ServerishError):
|
|
|
13
13
|
pass
|
|
14
14
|
|
|
15
15
|
class MessengerNotConnected(ServerishError):
|
|
16
|
+
"""Raised when an operation needs a NATS connection which is not there
|
|
17
|
+
|
|
18
|
+
Either the Messenger has not been opened, or the connection is gone
|
|
19
|
+
(see `MessengerConnectionClosed`).
|
|
20
|
+
"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MessengerTransportError(ServerishError):
|
|
25
|
+
"""Raised when the underlying NATS client fails to carry out an operation
|
|
26
|
+
|
|
27
|
+
Common base of all errors translated from the NATS client library. The
|
|
28
|
+
original exception is always available as ``__cause__``; serverish users
|
|
29
|
+
are not expected to inspect it, nor to know the client library at all.
|
|
30
|
+
"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MessengerConnectionClosed(MessengerTransportError, MessengerNotConnected):
|
|
35
|
+
"""Raised when the NATS connection is closed, draining, stale or has no server to talk to"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MessengerOutboundBufferFull(MessengerTransportError):
|
|
40
|
+
"""Raised when the client's outbound buffer overflows
|
|
41
|
+
|
|
42
|
+
Typically the connection is down (reconnecting) and messages published
|
|
43
|
+
in the meantime no longer fit the pending buffer.
|
|
44
|
+
"""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class MessengerJetStreamError(MessengerTransportError):
|
|
49
|
+
"""Raised when the JetStream API refuses or fails a request
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
code (int | None): HTTP-like status of the response (e.g. 404, 503)
|
|
53
|
+
err_code (int | None): JetStream specific error code
|
|
54
|
+
description (str | None): server-side error description
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, *args, code: int | None = None, err_code: int | None = None,
|
|
58
|
+
description: str | None = None) -> None:
|
|
59
|
+
super().__init__(*args)
|
|
60
|
+
self.code = code
|
|
61
|
+
self.err_code = err_code
|
|
62
|
+
self.description = description
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MessengerTransportTimeout(MessengerTransportError, TimeoutError):
|
|
66
|
+
"""Raised when the NATS server did not answer in time"""
|
|
16
67
|
pass
|
|
17
68
|
|
|
18
69
|
class MessengerReaderStopped(ServerishError):
|
|
@@ -52,7 +103,7 @@ class MessengerRequestNoResultYet(MessengerRequestNoResponse):
|
|
|
52
103
|
pass
|
|
53
104
|
|
|
54
105
|
|
|
55
|
-
class MessengerPublishAckTimeout(
|
|
106
|
+
class MessengerPublishAckTimeout(MessengerTransportTimeout):
|
|
56
107
|
"""Raised when JetStream did not confirm a publish within the ack timeout.
|
|
57
108
|
|
|
58
109
|
The message MAY have been delivered and stored — only the acknowledgement
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Translation of NATS client library errors into serverish exceptions
|
|
2
|
+
|
|
3
|
+
The single place where ``nats.errors`` / ``nats.js.errors`` exceptions are
|
|
4
|
+
mapped onto the `ServerishError` hierarchy. The messenger API never lets a
|
|
5
|
+
raw client-library exception out: whoever catches `ServerishError` handles
|
|
6
|
+
every failure serverish can report, without knowing which client library
|
|
7
|
+
(or which version of it) works underneath.
|
|
8
|
+
|
|
9
|
+
Use `nats_errors_translated` around calls into the client library at the
|
|
10
|
+
public boundary of a driver. Code which reacts to specific client errors
|
|
11
|
+
internally keeps catching the raw exceptions inside that boundary.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import contextlib
|
|
16
|
+
from typing import Iterator
|
|
17
|
+
|
|
18
|
+
import nats.errors
|
|
19
|
+
import nats.js.errors
|
|
20
|
+
|
|
21
|
+
from serverish.base.exceptions import (ServerishError, MessengerTransportError, MessengerConnectionClosed,
|
|
22
|
+
MessengerOutboundBufferFull, MessengerJetStreamError,
|
|
23
|
+
MessengerTransportTimeout)
|
|
24
|
+
|
|
25
|
+
_CONNECTION_GONE = (
|
|
26
|
+
nats.errors.ConnectionClosedError,
|
|
27
|
+
nats.errors.ConnectionDrainingError,
|
|
28
|
+
nats.errors.ConnectionReconnectingError,
|
|
29
|
+
nats.errors.StaleConnectionError,
|
|
30
|
+
nats.errors.UnexpectedEOF, # a StaleConnectionError subclass today - listed so the mapping does not depend on it
|
|
31
|
+
nats.errors.NoServersError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Status implied by the class of a JetStream API error; the client library
|
|
35
|
+
# does not always fill `code` in (e.g. errors it raises on its own)
|
|
36
|
+
_API_ERROR_CODES = (
|
|
37
|
+
(nats.js.errors.NotFoundError, 404),
|
|
38
|
+
(nats.js.errors.BadRequestError, 400),
|
|
39
|
+
(nats.js.errors.ServiceUnavailableError, 503),
|
|
40
|
+
(nats.js.errors.ServerError, 500),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def translate_nats_error(error: Exception, context: str) -> Exception:
|
|
45
|
+
"""Returns the serverish counterpart of a NATS client library exception
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
error: exception to translate
|
|
49
|
+
context: what serverish was doing, the lead of the resulting message
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
A `MessengerTransportError` (subclass) for a client library error -
|
|
53
|
+
raise it ``from error`` to keep the original as ``__cause__``.
|
|
54
|
+
Any other exception (serverish ones included) is returned unchanged.
|
|
55
|
+
"""
|
|
56
|
+
if isinstance(error, ServerishError) or not isinstance(error, nats.errors.Error):
|
|
57
|
+
return error
|
|
58
|
+
message = f"{context}: {error}"
|
|
59
|
+
if isinstance(error, _CONNECTION_GONE):
|
|
60
|
+
return MessengerConnectionClosed(message)
|
|
61
|
+
if isinstance(error, nats.errors.OutboundBufferLimitError):
|
|
62
|
+
return MessengerOutboundBufferFull(message)
|
|
63
|
+
if isinstance(error, nats.js.errors.APIError):
|
|
64
|
+
return MessengerJetStreamError(message, code=jetstream_error_code(error), err_code=error.err_code,
|
|
65
|
+
description=error.description)
|
|
66
|
+
if isinstance(error, nats.errors.TimeoutError):
|
|
67
|
+
return MessengerTransportTimeout(message)
|
|
68
|
+
return MessengerTransportError(message)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@contextlib.contextmanager
|
|
72
|
+
def nats_errors_translated(context: str) -> Iterator[None]:
|
|
73
|
+
"""Context manager translating NATS client library errors raised in its body
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
context: what serverish is doing, the lead of the resulting message
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
MessengerTransportError: (subclass) instead of any ``nats.errors.Error``
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
yield
|
|
83
|
+
except nats.errors.Error as e:
|
|
84
|
+
raise translate_nats_error(e, context) from e
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def jetstream_error_code(error: Exception) -> int | None:
|
|
88
|
+
"""Returns the HTTP-like status code of a JetStream API error, raw or translated"""
|
|
89
|
+
if isinstance(error, MessengerJetStreamError):
|
|
90
|
+
return error.code
|
|
91
|
+
if isinstance(error, nats.js.errors.APIError):
|
|
92
|
+
if error.code is not None:
|
|
93
|
+
return int(error.code)
|
|
94
|
+
for cls, code in _API_ERROR_CODES:
|
|
95
|
+
if isinstance(error, cls):
|
|
96
|
+
return code
|
|
97
|
+
return None
|
|
@@ -28,7 +28,8 @@ from serverish.base.idmanger import gen_id, gen_uid
|
|
|
28
28
|
from serverish.base.manageable import Manageable
|
|
29
29
|
from serverish.messenger.msgvalidator import MsgValidator
|
|
30
30
|
from serverish.base.singleton import Singleton
|
|
31
|
-
from serverish.base import MessengerCannotConnect
|
|
31
|
+
from serverish.base import MessengerCannotConnect, MessengerNotConnected
|
|
32
|
+
from serverish.connection.nats_errors import nats_errors_translated
|
|
32
33
|
|
|
33
34
|
if TYPE_CHECKING:
|
|
34
35
|
from serverish.messenger.msg_publisher import MsgPublisher
|
|
@@ -63,7 +64,8 @@ class Messenger(Singleton):
|
|
|
63
64
|
@property
|
|
64
65
|
def connection(self) -> ConnectionJetStream:
|
|
65
66
|
if self.conn is None:
|
|
66
|
-
raise
|
|
67
|
+
raise MessengerNotConnected("Messenger connection has not been opened, "
|
|
68
|
+
"use Messenger().open(host, port) or Messenger().context(host, port) first")
|
|
67
69
|
return self.conn
|
|
68
70
|
|
|
69
71
|
async def open(self, host: str | None = None, port: int | None = None,
|
|
@@ -319,8 +321,11 @@ class Messenger(Singleton):
|
|
|
319
321
|
|
|
320
322
|
async def purge(self, subject: str) -> None:
|
|
321
323
|
js = self.connection.js
|
|
322
|
-
|
|
323
|
-
|
|
324
|
+
if js is None:
|
|
325
|
+
raise MessengerNotConnected(f"Cannot purge subject '{subject}', JetStream not connected")
|
|
326
|
+
with nats_errors_translated(f"Purging subject '{subject}' failed"):
|
|
327
|
+
stream = await js.find_stream_name_by_subject(subject)
|
|
328
|
+
await js.purge_stream(stream, subject=subject)
|
|
324
329
|
|
|
325
330
|
@staticmethod
|
|
326
331
|
def get_publisher(subject: str) -> "MsgPublisher":
|
|
@@ -21,7 +21,12 @@ class MsgCallbackSubscriber(MsgReader):
|
|
|
21
21
|
"""
|
|
22
22
|
callback = param.Callable(default=None, doc="Callback function to call on each message")
|
|
23
23
|
task = param.ClassSelector(default=None, class_=Task, doc="Task for reading messages")
|
|
24
|
-
|
|
24
|
+
|
|
25
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
26
|
+
# per instance: a class-level default would be one Event shared by all subscribers,
|
|
27
|
+
# stopping one would stop them all
|
|
28
|
+
self._stop_event: Event = Event()
|
|
29
|
+
super().__init__(*args, **kwargs)
|
|
25
30
|
|
|
26
31
|
async def open(self) -> None:
|
|
27
32
|
return await super().open()
|
|
@@ -5,6 +5,7 @@ import jsonschema
|
|
|
5
5
|
import param
|
|
6
6
|
|
|
7
7
|
from serverish.base import MessengerNotConnected
|
|
8
|
+
from serverish.connection.nats_errors import translate_nats_error
|
|
8
9
|
from serverish.messenger.messenger import MsgDriver, log
|
|
9
10
|
|
|
10
11
|
|
|
@@ -15,7 +16,8 @@ class MsgCorePub(MsgDriver):
|
|
|
15
16
|
on subjects that are not backed by a JetStream stream.
|
|
16
17
|
|
|
17
18
|
Parameters:
|
|
18
|
-
raise_on_publish_error (bool): Raise on publish error
|
|
19
|
+
raise_on_publish_error (bool): Raise on publish error (default ``True``). When ``False``, errors
|
|
20
|
+
(a missing connection included) are logged and reported in the returned message instead
|
|
19
21
|
"""
|
|
20
22
|
|
|
21
23
|
raise_on_publish_error = param.Boolean(default=True, doc="Raise on publish error")
|
|
@@ -32,7 +34,11 @@ class MsgCorePub(MsgDriver):
|
|
|
32
34
|
|
|
33
35
|
Raises:
|
|
34
36
|
MessengerNotConnected: if the NATS connection is not available
|
|
35
|
-
|
|
37
|
+
MessengerTransportError: any other failure of the underlying NATS client
|
|
38
|
+
jsonschema.ValidationError: if the message fails schema validation (always raised)
|
|
39
|
+
|
|
40
|
+
With ``raise_on_publish_error`` set to False the serverish errors above are not raised:
|
|
41
|
+
the error is logged and the message is returned with the ``error`` tag.
|
|
36
42
|
"""
|
|
37
43
|
from serverish.messenger import Messenger
|
|
38
44
|
messenger: Messenger = self.messenger
|
|
@@ -51,22 +57,19 @@ class MsgCorePub(MsgDriver):
|
|
|
51
57
|
f"Trying to publish to subject '{self.subject}' failed. NATS not connected"
|
|
52
58
|
)
|
|
53
59
|
await nc.publish(self.subject, bdata)
|
|
54
|
-
except MessengerNotConnected:
|
|
55
|
-
raise
|
|
56
|
-
except AttributeError:
|
|
57
|
-
raise MessengerNotConnected(
|
|
58
|
-
f"Trying to publish to subject '{self.subject}' failed. NATS not connected"
|
|
59
|
-
)
|
|
60
60
|
except Exception as e:
|
|
61
|
+
error = translate_nats_error(e, f"Publishing message {msg['meta']['id']} to '{self.subject}' failed")
|
|
61
62
|
log.error(
|
|
62
63
|
f"Trying to publish to subject '{self.subject}' failed. "
|
|
63
|
-
f"Message {msg['meta']['id']} publish error: {
|
|
64
|
+
f"Message {msg['meta']['id']} publish error: {error}"
|
|
64
65
|
)
|
|
65
66
|
if self.raise_on_publish_error:
|
|
66
|
-
|
|
67
|
+
if error is e:
|
|
68
|
+
raise e
|
|
69
|
+
raise error from e
|
|
67
70
|
else:
|
|
68
71
|
msg['meta']['tags'].append('error')
|
|
69
|
-
msg['meta']['status'] = str(
|
|
72
|
+
msg['meta']['status'] = str(error)
|
|
70
73
|
return msg
|
|
71
74
|
|
|
72
75
|
|
|
@@ -6,7 +6,8 @@ import logging
|
|
|
6
6
|
from nats.aio.msg import Msg
|
|
7
7
|
from nats.aio.subscription import Subscription
|
|
8
8
|
|
|
9
|
-
from serverish.base.exceptions import MessengerReaderStopped
|
|
9
|
+
from serverish.base.exceptions import MessengerReaderStopped, MessengerNotConnected
|
|
10
|
+
from serverish.connection.nats_errors import nats_errors_translated
|
|
10
11
|
from serverish.messenger.messenger import MsgDriver
|
|
11
12
|
|
|
12
13
|
log = logging.getLogger(__name__.rsplit('.')[-1])
|
|
@@ -79,7 +80,8 @@ class MsgCoreReader(MsgDriver):
|
|
|
79
80
|
self.connection.remove_reconnect_cb(self._reconnect_cb)
|
|
80
81
|
self._reconnect_cb = None
|
|
81
82
|
if self.subscription is not None:
|
|
82
|
-
|
|
83
|
+
with nats_errors_translated(f"Unsubscribing from '{self.subject}' failed"):
|
|
84
|
+
await self.subscription.unsubscribe()
|
|
83
85
|
self.subscription = None
|
|
84
86
|
return await super().close()
|
|
85
87
|
|
|
@@ -90,6 +92,8 @@ class MsgCoreReader(MsgDriver):
|
|
|
90
92
|
async def _do_subscribe(self) -> None:
|
|
91
93
|
"""(Re-)create the raw NATS subscription, routing messages into the queue."""
|
|
92
94
|
nc = self.connection.nc
|
|
95
|
+
if nc is None:
|
|
96
|
+
raise MessengerNotConnected(f"Cannot subscribe to '{self.subject}', NATS not connected")
|
|
93
97
|
|
|
94
98
|
async def _cb(nats_msg: Msg) -> None:
|
|
95
99
|
if self._queue is None:
|
|
@@ -101,7 +105,8 @@ class MsgCoreReader(MsgDriver):
|
|
|
101
105
|
return
|
|
102
106
|
await self._queue.put((data, meta))
|
|
103
107
|
|
|
104
|
-
|
|
108
|
+
with nats_errors_translated(f"Subscribing to '{self.subject}' failed"):
|
|
109
|
+
self.subscription = await nc.subscribe(self.subject, cb=_cb)
|
|
105
110
|
|
|
106
111
|
# ------------------------------------------------------------------
|
|
107
112
|
# Reading
|
|
@@ -19,6 +19,7 @@ import param
|
|
|
19
19
|
from nats.js.kv import KeyValue
|
|
20
20
|
|
|
21
21
|
from serverish.base import MessengerKvBucketNotFound, MessengerKvMalformed, dt_ensure_array
|
|
22
|
+
from serverish.connection.nats_errors import nats_errors_translated
|
|
22
23
|
from serverish.messenger.messenger import MsgDriver
|
|
23
24
|
|
|
24
25
|
log = logging.getLogger(__name__.rsplit('.')[-1])
|
|
@@ -48,14 +49,15 @@ class MsgKvDriver(MsgDriver):
|
|
|
48
49
|
self.subject = f'$KV.{self.bucket}' # diagnostic only, KV entries are addressed by bucket/key
|
|
49
50
|
|
|
50
51
|
async def open(self) -> None:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
with nats_errors_translated(f"Opening KV bucket '{self.bucket}' failed"):
|
|
53
|
+
try:
|
|
54
|
+
self.kv = await self.connection.ensure_kv_bucket(self.bucket,
|
|
55
|
+
create_if_needed=self.create_bucket,
|
|
56
|
+
**self.bucket_config)
|
|
57
|
+
except nats.js.errors.BucketNotFoundError as e:
|
|
58
|
+
raise MessengerKvBucketNotFound(
|
|
59
|
+
f"KV bucket '{self.bucket}' does not exist, "
|
|
60
|
+
f"create it externally or open the driver with create_bucket=True") from e
|
|
59
61
|
await super().open()
|
|
60
62
|
|
|
61
63
|
async def close(self) -> None:
|
|
@@ -4,6 +4,7 @@ import param
|
|
|
4
4
|
from nats.js.kv import KeyValue
|
|
5
5
|
|
|
6
6
|
from serverish.base import MessengerReaderStopped
|
|
7
|
+
from serverish.connection.nats_errors import nats_errors_translated
|
|
7
8
|
from serverish.messenger.messenger import Messenger
|
|
8
9
|
from serverish.messenger.msg_kv import MsgKvDriver, log
|
|
9
10
|
|
|
@@ -34,9 +35,10 @@ class MsgKvReader(MsgKvDriver):
|
|
|
34
35
|
|
|
35
36
|
async def open(self) -> None:
|
|
36
37
|
await super().open()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
with nats_errors_translated(f"Watching KV {self.bucket}/{self.key} failed"):
|
|
39
|
+
self._watcher = await self.kv.watch(self.key,
|
|
40
|
+
include_history=self.include_history,
|
|
41
|
+
ignore_deletes=self.ignore_deletes)
|
|
40
42
|
|
|
41
43
|
async def close(self) -> None:
|
|
42
44
|
if self._watcher is not None:
|
|
@@ -54,7 +56,8 @@ class MsgKvReader(MsgKvDriver):
|
|
|
54
56
|
if self._watcher is None:
|
|
55
57
|
raise MessengerReaderStopped(f"KV reader {self} is not open")
|
|
56
58
|
while True:
|
|
57
|
-
|
|
59
|
+
with nats_errors_translated(f"Watching KV {self.bucket}/{self.key} failed"):
|
|
60
|
+
entry = await self._watcher.__anext__() # raises StopAsyncIteration when watcher is stopped
|
|
58
61
|
if entry is None:
|
|
59
62
|
# nats-py sends a None marker once the initial replay is done, it is not a value
|
|
60
63
|
continue
|
|
@@ -4,6 +4,7 @@ import nats.js.errors
|
|
|
4
4
|
from nats.js.kv import KeyValue
|
|
5
5
|
|
|
6
6
|
from serverish.base import MessengerKvKeyNotFound
|
|
7
|
+
from serverish.connection.nats_errors import nats_errors_translated
|
|
7
8
|
from serverish.messenger.messenger import Messenger, MsgDriver
|
|
8
9
|
from serverish.messenger.msg_kv import MsgKvDriver, log
|
|
9
10
|
|
|
@@ -31,7 +32,8 @@ class MsgKvStore(MsgKvDriver):
|
|
|
31
32
|
dict: published message, with meta['kv'] carrying bucket/key/revision
|
|
32
33
|
"""
|
|
33
34
|
msg, bdata = self.encode_envelope(data, meta)
|
|
34
|
-
|
|
35
|
+
with nats_errors_translated(f"KV PUT {self.bucket}/{key} failed"):
|
|
36
|
+
revision = await self.kv.put(key, bdata)
|
|
35
37
|
msg['meta']['kv'] = {'bucket': self.bucket, 'key': key, 'revision': revision, 'operation': 'PUT'}
|
|
36
38
|
self.messenger.log_msg_trace(msg.get('data', {}), msg['meta'], f"KV PUT {self.bucket}/{key}")
|
|
37
39
|
return msg
|
|
@@ -48,10 +50,11 @@ class MsgKvStore(MsgKvDriver):
|
|
|
48
50
|
MessengerKvKeyNotFound: key does not exist (or is deleted)
|
|
49
51
|
MessengerKvMalformed: entry is not a serverish envelope
|
|
50
52
|
"""
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
with nats_errors_translated(f"KV GET {self.bucket}/{key} failed"):
|
|
54
|
+
try:
|
|
55
|
+
entry = await self.kv.get(key, revision=revision)
|
|
56
|
+
except nats.js.errors.KeyNotFoundError as e:
|
|
57
|
+
raise MessengerKvKeyNotFound(f"Key '{key}' not found in KV bucket '{self.bucket}'") from e
|
|
55
58
|
data, meta = self.decode_entry(entry)
|
|
56
59
|
self.messenger.log_msg_trace(data, meta, f"KV GET {self.bucket}/{key}")
|
|
57
60
|
return data, meta
|
|
@@ -59,22 +62,25 @@ class MsgKvStore(MsgKvDriver):
|
|
|
59
62
|
@MsgDriver.ensure_open
|
|
60
63
|
async def delete(self, key: str) -> None:
|
|
61
64
|
"""Places a delete marker for the key and removes previous revisions"""
|
|
62
|
-
|
|
65
|
+
with nats_errors_translated(f"KV DEL {self.bucket}/{key} failed"):
|
|
66
|
+
await self.kv.delete(key)
|
|
63
67
|
log.debug(f"KV DEL {self.bucket}/{key}")
|
|
64
68
|
|
|
65
69
|
@MsgDriver.ensure_open
|
|
66
70
|
async def purge(self, key: str) -> None:
|
|
67
71
|
"""Removes the key including all its revisions"""
|
|
68
|
-
|
|
72
|
+
with nats_errors_translated(f"KV PURGE {self.bucket}/{key} failed"):
|
|
73
|
+
await self.kv.purge(key)
|
|
69
74
|
log.debug(f"KV PURGE {self.bucket}/{key}")
|
|
70
75
|
|
|
71
76
|
@MsgDriver.ensure_open
|
|
72
77
|
async def keys(self) -> list[str]:
|
|
73
78
|
"""Returns list of keys in the bucket, empty list for an empty bucket"""
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
with nats_errors_translated(f"Listing keys of KV bucket '{self.bucket}' failed"):
|
|
80
|
+
try:
|
|
81
|
+
return await self.kv.keys()
|
|
82
|
+
except nats.js.errors.NoKeysError:
|
|
83
|
+
return []
|
|
78
84
|
|
|
79
85
|
@MsgDriver.ensure_open
|
|
80
86
|
async def history(self, key: str) -> list[tuple[dict | None, dict]]:
|
|
@@ -86,16 +92,18 @@ class MsgKvStore(MsgKvDriver):
|
|
|
86
92
|
Raises:
|
|
87
93
|
MessengerKvKeyNotFound: key has no history in the bucket
|
|
88
94
|
"""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
with nats_errors_translated(f"KV history of {self.bucket}/{key} failed"):
|
|
96
|
+
try:
|
|
97
|
+
entries = await self.kv.history(key)
|
|
98
|
+
except nats.js.errors.NoKeysError as e:
|
|
99
|
+
raise MessengerKvKeyNotFound(f"Key '{key}' has no history in KV bucket '{self.bucket}'") from e
|
|
93
100
|
return [self.decode_entry(entry) for entry in entries]
|
|
94
101
|
|
|
95
102
|
@MsgDriver.ensure_open
|
|
96
103
|
async def status(self) -> KeyValue.BucketStatus:
|
|
97
104
|
"""Returns the status of the underlying KV bucket"""
|
|
98
|
-
|
|
105
|
+
with nats_errors_translated(f"Status of KV bucket '{self.bucket}' failed"):
|
|
106
|
+
return await self.kv.status()
|
|
99
107
|
|
|
100
108
|
|
|
101
109
|
def get_kvstore(bucket: str, **kwargs) -> MsgKvStore:
|
|
@@ -8,6 +8,7 @@ import nats.js
|
|
|
8
8
|
import param
|
|
9
9
|
|
|
10
10
|
from serverish.base import MessengerNotConnected, MessengerPublishAckTimeout
|
|
11
|
+
from serverish.connection.nats_errors import translate_nats_error
|
|
11
12
|
from serverish.messenger import Messenger
|
|
12
13
|
from serverish.messenger.messenger import MsgDriver, log
|
|
13
14
|
|
|
@@ -26,7 +27,8 @@ class MsgPublisher(MsgDriver):
|
|
|
26
27
|
processed too late), the retry is discarded by the server.
|
|
27
28
|
|
|
28
29
|
Parameters:
|
|
29
|
-
raise_on_publish_error (bool): Raise on publish error
|
|
30
|
+
raise_on_publish_error (bool): Raise on publish error (default `True`). When `False`, errors
|
|
31
|
+
(a missing connection included) are logged and reported in the returned message instead
|
|
30
32
|
ack_timeout_retries (int): How many times to retry a publish for which no JetStream
|
|
31
33
|
ack arrived in time (default 2). Retries are deduplicated server-side via
|
|
32
34
|
``Nats-Msg-Id``. When retries are exhausted, `MessengerPublishAckTimeout` is
|
|
@@ -63,8 +65,14 @@ class MsgPublisher(MsgDriver):
|
|
|
63
65
|
Returns:
|
|
64
66
|
dict: published message, or message to be published with the `error` tag in meta if failed
|
|
65
67
|
Raises:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
MessengerNotConnected: no connection to publish through
|
|
69
|
+
MessengerPublishAckTimeout: JetStream did not confirm the publish (after retries)
|
|
70
|
+
MessengerTransportError: any other failure of the underlying NATS client
|
|
71
|
+
jsonschema.ValidationError: the message does not match its schema (always raised)
|
|
72
|
+
|
|
73
|
+
With `raise_on_publish_error` set to False the serverish errors above are not raised:
|
|
74
|
+
the error is logged and the message is returned with the `error` tag and `status`
|
|
75
|
+
set to the error message.
|
|
68
76
|
"""
|
|
69
77
|
msg = self.messenger.create_msg(data, meta)
|
|
70
78
|
bdata = self.messenger.encode(msg)
|
|
@@ -85,11 +93,6 @@ class MsgPublisher(MsgDriver):
|
|
|
85
93
|
# Track successful publish
|
|
86
94
|
self._publish_count += 1
|
|
87
95
|
self._last_publish_time = time.monotonic()
|
|
88
|
-
except AttributeError as e: # no js - not connected
|
|
89
|
-
self._error_count += 1
|
|
90
|
-
self._last_error = e
|
|
91
|
-
log.error(f"Trying to publish to subject '{self.subject}' failed. JetStream not connected")
|
|
92
|
-
raise MessengerNotConnected(f"Trying to publish to subject '{self.subject}' failed. JetStream not connected")
|
|
93
96
|
except (nats.errors.NoRespondersError, nats.js.errors.NoStreamResponseError):
|
|
94
97
|
# it's OK for non-jetstream, we just don't have subscribers yet
|
|
95
98
|
# Still count as successful publish (message was sent, just no stream to persist)
|
|
@@ -99,15 +102,18 @@ class MsgPublisher(MsgDriver):
|
|
|
99
102
|
f'No subscribers yet returned by NATS server for subject {self.subject}, '
|
|
100
103
|
f'if it was meant to be jetstream, the subject does not exist in any stream!')
|
|
101
104
|
except Exception as e:
|
|
105
|
+
error = translate_nats_error(e, f"Publishing message {msg['meta']['id']} to '{self.subject}' failed")
|
|
102
106
|
self._error_count += 1
|
|
103
|
-
self._last_error =
|
|
107
|
+
self._last_error = error
|
|
104
108
|
log.error(f"Trying to publish to subject '{self.subject}' failed. "
|
|
105
|
-
f"Message {msg['meta']['id']} publish error: {
|
|
109
|
+
f"Message {msg['meta']['id']} publish error: {error}")
|
|
106
110
|
if self.raise_on_publish_error:
|
|
107
|
-
|
|
111
|
+
if error is e:
|
|
112
|
+
raise e
|
|
113
|
+
raise error from e
|
|
108
114
|
else:
|
|
109
115
|
msg['meta']['tags'].append('error')
|
|
110
|
-
msg['meta']['status'] = str(
|
|
116
|
+
msg['meta']['status'] = str(error)
|
|
111
117
|
return msg
|
|
112
118
|
|
|
113
119
|
async def _publish_with_ack_retries(self, bdata: bytes, headers: dict, msg_id: str, **kwargs) -> None:
|
|
@@ -119,11 +125,16 @@ class MsgPublisher(MsgDriver):
|
|
|
119
125
|
Nats-Msg-Id header, so this never produces duplicates.
|
|
120
126
|
|
|
121
127
|
Raises:
|
|
128
|
+
MessengerNotConnected: JetStream is not connected
|
|
122
129
|
MessengerPublishAckTimeout: no ack after all retries
|
|
123
130
|
"""
|
|
131
|
+
js = self.connection.js
|
|
132
|
+
if js is None:
|
|
133
|
+
raise MessengerNotConnected(f"Trying to publish to subject '{self.subject}' failed. "
|
|
134
|
+
f"JetStream not connected")
|
|
124
135
|
for attempt in range(self.ack_timeout_retries + 1):
|
|
125
136
|
try:
|
|
126
|
-
await
|
|
137
|
+
await js.publish(self.subject, bdata, headers=headers, **kwargs)
|
|
127
138
|
return
|
|
128
139
|
except nats.errors.TimeoutError as e:
|
|
129
140
|
if attempt < self.ack_timeout_retries:
|
|
@@ -19,9 +19,10 @@ from nats.js.api import DeliverPolicy, ConsumerConfig
|
|
|
19
19
|
|
|
20
20
|
from serverish.base import wait_for_psce
|
|
21
21
|
from serverish.base.exceptions import (MessengerReaderStopped, MessengerReaderConfigError,
|
|
22
|
-
MessengerNotConnected,
|
|
22
|
+
MessengerNotConnected, MessengerJetStreamError,
|
|
23
23
|
MessengerReaderAlreadyOpen, MessengerRequestTimeout)
|
|
24
24
|
from serverish.base.fifoset import FifoSet
|
|
25
|
+
from serverish.connection.nats_errors import nats_errors_translated, translate_nats_error
|
|
25
26
|
from serverish.messenger import Messenger
|
|
26
27
|
from serverish.messenger.messenger import MsgDriver
|
|
27
28
|
|
|
@@ -141,6 +142,11 @@ class MsgReader(MsgDriver):
|
|
|
141
142
|
Only stop signal and Cancel exception finish the iteration.
|
|
142
143
|
One may change this behavior by setting `on_connection_close` to 'RAISE' or 'FINISH'.
|
|
143
144
|
This will cause raising exception or finishing iteration on seruios erros.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
MessengerReaderStopped: the reader has been stopped (or finished, see `nowait`)
|
|
148
|
+
MessengerReaderConfigError: the server rejected the reader configuration, retrying is pointless
|
|
149
|
+
MessengerTransportError: (`error_behavior='RAISE'` only) failure of the underlying NATS client
|
|
144
150
|
"""
|
|
145
151
|
|
|
146
152
|
# The reader loop's state and methods
|
|
@@ -401,8 +407,10 @@ class MsgReader(MsgDriver):
|
|
|
401
407
|
# Fatal NATS API errors (4xx): the same request will never
|
|
402
408
|
# succeed — stop retrying immediately and surface a clear
|
|
403
409
|
# MessengerReaderConfigError to the caller.
|
|
404
|
-
|
|
405
|
-
|
|
410
|
+
# (open() reports client errors translated, the original is its cause)
|
|
411
|
+
cause = e.error.__cause__ if isinstance(e.error, MessengerJetStreamError) else e.error
|
|
412
|
+
if isinstance(cause, (nats.js.errors.BadRequestError,
|
|
413
|
+
nats.js.errors.NotFoundError)):
|
|
406
414
|
log.error(st.fmt(f"fatal NATS error (not retrying): {e.error}"))
|
|
407
415
|
self._stop.set()
|
|
408
416
|
raise MessengerReaderConfigError(
|
|
@@ -411,7 +419,10 @@ class MsgReader(MsgDriver):
|
|
|
411
419
|
match self.error_behavior:
|
|
412
420
|
case 'RAISE':
|
|
413
421
|
log.error(st.fmt(f"raising read_next error: {e.error}"))
|
|
414
|
-
|
|
422
|
+
error = translate_nats_error(e.error, f"Reading from '{self.subject}' failed")
|
|
423
|
+
if error is e.error:
|
|
424
|
+
raise e.error
|
|
425
|
+
raise error from e.error
|
|
415
426
|
case 'FINISH':
|
|
416
427
|
log.error(st.fmt(f"finishing iteration on error: {e.error}"))
|
|
417
428
|
raise MessengerReaderStopped
|
|
@@ -622,6 +633,14 @@ class MsgReader(MsgDriver):
|
|
|
622
633
|
ps._pending_next_msgs_calls.pop(task_name, None)
|
|
623
634
|
|
|
624
635
|
async def open(self) -> None:
|
|
636
|
+
"""Opens the reader: creates the JetStream consumer and subscribes to it
|
|
637
|
+
|
|
638
|
+
Raises:
|
|
639
|
+
MessengerReaderAlreadyOpen: the reader is open already
|
|
640
|
+
MessengerNotConnected: no connection to subscribe through
|
|
641
|
+
MessengerRequestTimeout: the server did not create the consumer in time
|
|
642
|
+
MessengerTransportError: any other failure of the underlying NATS client
|
|
643
|
+
"""
|
|
625
644
|
if self.pull_subscription is not None:
|
|
626
645
|
raise MessengerReaderAlreadyOpen("Reader already open, do not reuse MsgReader instances")
|
|
627
646
|
|
|
@@ -652,13 +671,15 @@ class MsgReader(MsgDriver):
|
|
|
652
671
|
# consumer_conf.durable_name = self.name if consumer_conf.durable_name is None else consumer_conf.durable_name
|
|
653
672
|
if self.connection is None or self.connection.js is None:
|
|
654
673
|
raise MessengerNotConnected('Cannot create pull subscription, not connected to NATS')
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
674
|
+
with nats_errors_translated(f"Creating consumer for '{self.subject}' failed"):
|
|
675
|
+
try:
|
|
676
|
+
ret = await self.connection.js.pull_subscribe(self.subject,
|
|
677
|
+
durable=consumer_conf.durable_name,
|
|
678
|
+
config=consumer_conf)
|
|
679
|
+
except nats.errors.TimeoutError as e:
|
|
680
|
+
raise MessengerRequestTimeout(
|
|
681
|
+
f"Creating consumer for '{self.subject}' timed out: {e}") from e
|
|
682
|
+
ci = await ret.consumer_info()
|
|
662
683
|
return ret
|
|
663
684
|
|
|
664
685
|
async def _reopen(self) -> None:
|
|
@@ -3,10 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from asyncio import CancelledError
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import nats.errors
|
|
7
7
|
|
|
8
8
|
from serverish.base import MessengerRequestNoResponders, MessengerRequestCanceled, MessengerRequestNoResultYet, \
|
|
9
|
-
MessengerRequestTimeout, MessengerRequestJetStreamSubject
|
|
9
|
+
MessengerRequestTimeout, MessengerRequestJetStreamSubject, MessengerNotConnected
|
|
10
|
+
from serverish.connection.nats_errors import nats_errors_translated
|
|
10
11
|
from serverish.base import Task, create_task
|
|
11
12
|
from serverish.messenger import Messenger
|
|
12
13
|
from serverish.messenger.messenger import MsgDriver
|
|
@@ -44,18 +45,21 @@ class MsgRpcRequester(MsgDriver):
|
|
|
44
45
|
Raises:
|
|
45
46
|
MessengerRequestNoResponders: if no responders were found for the request
|
|
46
47
|
MessengerRequestTimeout: if no response was received in time
|
|
48
|
+
MessengerNotConnected: no connection to send the request through
|
|
49
|
+
MessengerTransportError: any other failure of the underlying NATS client
|
|
47
50
|
"""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
nc = self.connection.nc
|
|
52
|
+
if nc is None:
|
|
53
|
+
raise MessengerNotConnected(f"Request to '{self.subject}' failed. NATS not connected")
|
|
51
54
|
msg = self.messenger.create_msg(data, meta=meta)
|
|
52
55
|
bmsg = self.messenger.encode(msg)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
with nats_errors_translated(f"Request to '{self.subject}' failed"):
|
|
57
|
+
try:
|
|
58
|
+
resp = await nc.request(self.subject, payload=bmsg, timeout=timeout)
|
|
59
|
+
except nats.errors.NoRespondersError as e:
|
|
60
|
+
raise MessengerRequestNoResponders(f"No responders for request to '{self.subject}'") from e
|
|
61
|
+
except nats.errors.TimeoutError as e:
|
|
62
|
+
raise MessengerRequestTimeout(f"No response to request to '{self.subject}' in time") from e
|
|
59
63
|
rdata, rmeta = self.messenger.unpack_nats_msg(resp)
|
|
60
64
|
if rmeta is None:
|
|
61
65
|
raise MessengerRequestJetStreamSubject(self.subject)
|
|
@@ -8,6 +8,8 @@ from typing import Callable
|
|
|
8
8
|
from nats.aio.msg import Msg
|
|
9
9
|
from nats.aio.subscription import Subscription
|
|
10
10
|
|
|
11
|
+
from serverish.base import MessengerNotConnected
|
|
12
|
+
from serverish.connection.nats_errors import nats_errors_translated
|
|
11
13
|
from serverish.messenger import Messenger
|
|
12
14
|
from serverish.messenger.messenger import MsgDriver
|
|
13
15
|
|
|
@@ -44,7 +46,8 @@ class Rpc:
|
|
|
44
46
|
messenger = Messenger() # this is singleton anyway
|
|
45
47
|
msg = messenger.create_msg(data=self.resp_data, meta=self.resp_meta)
|
|
46
48
|
bmsg = messenger.encode(msg)
|
|
47
|
-
|
|
49
|
+
with nats_errors_translated(f"Responding to request on '{self.nats_msg.subject}' failed"):
|
|
50
|
+
await self.nats_msg.respond(bmsg)
|
|
48
51
|
|
|
49
52
|
|
|
50
53
|
class MsgRpcResponder(MsgDriver):
|
|
@@ -120,9 +123,9 @@ class MsgRpcResponder(MsgDriver):
|
|
|
120
123
|
|
|
121
124
|
async def _register_function_internal(self, callback: Callable[[Rpc], None] | Callable[[Rpc], asyncio.Future]):
|
|
122
125
|
"""Internal method to register the callback with NATS subscription"""
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
nc = self.connection.nc
|
|
127
|
+
if nc is None:
|
|
128
|
+
raise MessengerNotConnected(f"Cannot subscribe RPC responder for '{self.subject}', NATS not connected")
|
|
126
129
|
if asyncio.iscoroutinefunction(callback):
|
|
127
130
|
acb = callback
|
|
128
131
|
scb = None
|
|
@@ -152,13 +155,15 @@ class MsgRpcResponder(MsgDriver):
|
|
|
152
155
|
await rpc.send_response()
|
|
153
156
|
|
|
154
157
|
|
|
155
|
-
|
|
158
|
+
with nats_errors_translated(f"Subscribing RPC responder for '{self.subject}' failed"):
|
|
159
|
+
self.subscription = await nc.subscribe(self.subject, queue=self.subject, cb=_cb)
|
|
156
160
|
|
|
157
161
|
async def close(self) -> None:
|
|
158
162
|
"""Close the responder and unregister from reconnection callbacks"""
|
|
159
163
|
self.connection.remove_reconnect_cb(self.on_nats_reconnect)
|
|
160
164
|
if self.subscription is not None:
|
|
161
|
-
|
|
165
|
+
with nats_errors_translated(f"Unsubscribing RPC responder for '{self.subject}' failed"):
|
|
166
|
+
await self.subscription.unsubscribe()
|
|
162
167
|
self._callback = None # Clear callback reference
|
|
163
168
|
return await super().close()
|
|
164
169
|
|
|
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
|