serverish 1.2.2__tar.gz → 1.4.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-1.2.2 → serverish-1.4.0}/PKG-INFO +4 -5
- {serverish-1.2.2 → serverish-1.4.0}/README.md +2 -1
- {serverish-1.2.2 → serverish-1.4.0}/pyproject.toml +2 -2
- {serverish-1.2.2 → serverish-1.4.0}/serverish/base/exceptions.py +5 -1
- serverish-1.4.0/serverish/base/iterators.py +97 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/connection/connection_jets.py +8 -3
- {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_reader.py +92 -28
- {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_rpc_resp.py +2 -2
- {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_single_read.py +23 -1
- {serverish-1.2.2 → serverish-1.4.0}/LICENSE +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/__init__.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/base/__init__.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/base/asyncio_util_functions.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/base/collector.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/base/datetime.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/base/fifoset.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/base/hasstatuses.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/base/idmanger.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/base/manageable.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/base/singleton.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/base/status.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/base/task_manager.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/connection/__init__.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/connection/connection.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/connection/connection_nats.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/__init__.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/logging.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/messenger.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_callback_sub.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_journal_pub.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_journal_read.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_progress_pub.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_progress_read.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_publisher.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_rpc_req.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_single_pub.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msgvalidator.py +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/config.schema.json +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/default.schema.json +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/include/timestamp.schema.json +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/journal.schema.json +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/meta.schema.json +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/program_current_state.schema.json +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/program_state.schema.json +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/progress.schema.json +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/telemetry.schema.json +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/services/TODO.txt +0 -0
- {serverish-1.2.2 → serverish-1.4.0}/serverish/slog.py +0 -0
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: serverish
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: helpers for server alike projects
|
|
5
5
|
Home-page: https://github.com/AkondLab/serverish
|
|
6
6
|
Author: Mikołaj Kałuszyński
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
11
9
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
10
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -32,7 +30,7 @@ Description-Content-Type: text/markdown
|
|
|
32
30
|
See [`doc` directory](doc/) for more documentation.
|
|
33
31
|
|
|
34
32
|
## Optional (extras) dependencies
|
|
35
|
-
|
|
33
|
+
The following extras are available:
|
|
36
34
|
* `messenger` - for using NATS based Messenger. Will install `nats-py` package.
|
|
37
35
|
* `dns` - for using `aiodns` DNS resolver for connection status diagnostics, it's extracted as an extra dependency because it's not available on all platforms.
|
|
38
36
|
|
|
@@ -49,6 +47,7 @@ this will install `nats-py` package.
|
|
|
49
47
|
- Pytest fixtures for NATS on CI
|
|
50
48
|
|
|
51
49
|
## Changes
|
|
50
|
+
* 1.4 Contains improvements to the network problems recovery and connection tracking.
|
|
52
51
|
* 1.1 Switches to `param` 2.* nad `py-nats` 1.7.*. Also, publisher may reraise exceptions or ignore them.
|
|
53
52
|
* 1.0 Changes NATS subscriptions to pull only
|
|
54
53
|
* 0.12 Extends messges metdata of `nats` section, Handles reconnection of subscriptions,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
See [`doc` directory](doc/) for more documentation.
|
|
11
11
|
|
|
12
12
|
## Optional (extras) dependencies
|
|
13
|
-
|
|
13
|
+
The following extras are available:
|
|
14
14
|
* `messenger` - for using NATS based Messenger. Will install `nats-py` package.
|
|
15
15
|
* `dns` - for using `aiodns` DNS resolver for connection status diagnostics, it's extracted as an extra dependency because it's not available on all platforms.
|
|
16
16
|
|
|
@@ -27,6 +27,7 @@ this will install `nats-py` package.
|
|
|
27
27
|
- Pytest fixtures for NATS on CI
|
|
28
28
|
|
|
29
29
|
## Changes
|
|
30
|
+
* 1.4 Contains improvements to the network problems recovery and connection tracking.
|
|
30
31
|
* 1.1 Switches to `param` 2.* nad `py-nats` 1.7.*. Also, publisher may reraise exceptions or ignore them.
|
|
31
32
|
* 1.0 Changes NATS subscriptions to pull only
|
|
32
33
|
* 0.12 Extends messges metdata of `nats` section, Handles reconnection of subscriptions,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "serverish"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.4.0"
|
|
4
4
|
description = "helpers for server alike projects"
|
|
5
5
|
authors = ["Mikołaj Kałuszyński", "MMME team"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -8,7 +8,7 @@ repository = "https://github.com/AkondLab/serverish"
|
|
|
8
8
|
include = ["serverish/schema/*"]
|
|
9
9
|
|
|
10
10
|
[tool.poetry.dependencies]
|
|
11
|
-
python = "^3.
|
|
11
|
+
python = "^3.10"
|
|
12
12
|
aiodns = {version="^3.2.0", optional=true}
|
|
13
13
|
param = "^2.1.0"
|
|
14
14
|
nats-py = { version = "^2.7.2", optional= true}
|
|
@@ -7,8 +7,12 @@ class MessengerNotConnected(Exception):
|
|
|
7
7
|
class MessengerReaderStopped(Exception):
|
|
8
8
|
pass
|
|
9
9
|
|
|
10
|
+
class MessengerReaderAlreadyOpen(RuntimeError):
|
|
11
|
+
pass
|
|
12
|
+
|
|
10
13
|
class MessengerRequestNoResponse(Exception):
|
|
11
14
|
pass
|
|
15
|
+
|
|
12
16
|
class MessengerRequestNoResponders(MessengerRequestNoResponse):
|
|
13
17
|
pass
|
|
14
18
|
|
|
@@ -21,7 +25,7 @@ class MessengerRequestJetStreamSubject(MessengerRequestNoResponse):
|
|
|
21
25
|
class MessengerRequestCanceled(MessengerRequestNoResponse):
|
|
22
26
|
pass
|
|
23
27
|
|
|
24
|
-
class MessengerRequestTimeout(MessengerRequestNoResponse):
|
|
28
|
+
class MessengerRequestTimeout(MessengerRequestNoResponse, TimeoutError):
|
|
25
29
|
pass
|
|
26
30
|
|
|
27
31
|
class MessengerRequestNoResultYet(MessengerRequestNoResponse):
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import List, Dict, Any, Tuple, Set
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AsyncRangeIter:
|
|
6
|
+
"""This class represent async range iterator (instead of sync).
|
|
7
|
+
|
|
8
|
+
WARNING: Noticed that end is included (not like in standard iterator).
|
|
9
|
+
Example 1: start=1, end=4, will return [1, 2, 3, 4]
|
|
10
|
+
Example 2: async for n in AsyncRangeIter(start=1, end=3): ...
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, start: int, end: int) -> None:
|
|
14
|
+
self.start = start
|
|
15
|
+
self.end = end
|
|
16
|
+
|
|
17
|
+
def __aiter__(self) -> Any:
|
|
18
|
+
self.current = self.start
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
async def __anext__(self) -> int:
|
|
22
|
+
if self.current <= self.end:
|
|
23
|
+
value = self.current
|
|
24
|
+
self.current += 1
|
|
25
|
+
await asyncio.sleep(0)
|
|
26
|
+
return value
|
|
27
|
+
else:
|
|
28
|
+
raise StopAsyncIteration
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AsyncListIter:
|
|
32
|
+
"""This class represent async list iterator (instead of sync).
|
|
33
|
+
|
|
34
|
+
Example of use: async for n in AsyncListIter(some_list): ...
|
|
35
|
+
"""
|
|
36
|
+
def __init__(self, iterable: List | Tuple | Set | Any):
|
|
37
|
+
self.iterable = iterable
|
|
38
|
+
self.index: int = 0
|
|
39
|
+
|
|
40
|
+
def __aiter__(self) -> Any:
|
|
41
|
+
self.index = 0
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
async def __anext__(self):
|
|
45
|
+
if self.index < len(self.iterable):
|
|
46
|
+
value = self.iterable[self.index]
|
|
47
|
+
self.index += 1
|
|
48
|
+
await asyncio.sleep(0)
|
|
49
|
+
return value
|
|
50
|
+
else:
|
|
51
|
+
raise StopAsyncIteration
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AsyncEnumerateIter:
|
|
55
|
+
"""This class represent async enumerate iterator (instead of sync).
|
|
56
|
+
|
|
57
|
+
Example of use: async for current_index, value in AsyncEnumerateIter(some_iterable): ...
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, iterable: List | Tuple | Set | Any) -> None:
|
|
61
|
+
self.iterable = iterable
|
|
62
|
+
|
|
63
|
+
def __aiter__(self):
|
|
64
|
+
self.index = 0
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
async def __anext__(self):
|
|
68
|
+
if self.index < len(self.iterable):
|
|
69
|
+
value = self.iterable[self.index]
|
|
70
|
+
current_index = self.index
|
|
71
|
+
self.index += 1
|
|
72
|
+
await asyncio.sleep(0)
|
|
73
|
+
return current_index, value
|
|
74
|
+
else:
|
|
75
|
+
raise StopAsyncIteration
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AsyncDictItemsIter:
|
|
79
|
+
"""This class represent async dict items iterator (instead of sync).
|
|
80
|
+
|
|
81
|
+
Example of use: async for key, value in AsyncEnumerateIter(some_dict): ...
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, data_dict: Dict) -> None:
|
|
85
|
+
self.data_dict = data_dict
|
|
86
|
+
|
|
87
|
+
def __aiter__(self) -> Any:
|
|
88
|
+
self.iterator = iter(self.data_dict.items())
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
async def __anext__(self) -> Tuple:
|
|
92
|
+
try:
|
|
93
|
+
n, m = next(self.iterator)
|
|
94
|
+
await asyncio.sleep(0)
|
|
95
|
+
return n, m
|
|
96
|
+
except StopIteration:
|
|
97
|
+
raise StopAsyncIteration
|
|
@@ -89,9 +89,14 @@ class ConnectionJetStream(ConnectionNATS):
|
|
|
89
89
|
|
|
90
90
|
async def nats_reconnected_cb(self):
|
|
91
91
|
await super().nats_reconnected_cb()
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
try:
|
|
93
|
+
self.setup_jetstream()
|
|
94
|
+
await self.update_statuses()
|
|
95
|
+
logging.info(f'JetStream reconnect status: {self.format_status()}')
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logging.error(f'Error setting up JetStream on reconnection cb: {e}')
|
|
98
|
+
# Update status to reflect the error
|
|
99
|
+
await self.update_statuses()
|
|
95
100
|
|
|
96
101
|
async def ensure_subject_in_stream(self, stream: str, subject: str,
|
|
97
102
|
create_stram_if_needed: bool = False,
|
|
@@ -17,7 +17,8 @@ from nats.js import JetStreamContext
|
|
|
17
17
|
from nats.js.api import DeliverPolicy, ConsumerConfig
|
|
18
18
|
|
|
19
19
|
from serverish.base import wait_for_psce
|
|
20
|
-
from serverish.base.exceptions import MessengerReaderStopped
|
|
20
|
+
from serverish.base.exceptions import (MessengerReaderStopped, MessengerNotConnected,
|
|
21
|
+
MessengerReaderAlreadyOpen, MessengerRequestTimeout)
|
|
21
22
|
from serverish.base.fifoset import FifoSet
|
|
22
23
|
from serverish.messenger import Messenger
|
|
23
24
|
from serverish.messenger.messenger import MsgDriver
|
|
@@ -154,14 +155,24 @@ class MsgReader(MsgDriver):
|
|
|
154
155
|
await bmsg.ack()
|
|
155
156
|
except Exception as e:
|
|
156
157
|
log.warning(self.fmt(f"Error acking message: {e}"))
|
|
157
|
-
|
|
158
|
+
# Not essential - we can continue without acking
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
data, meta = self.reader.messenger.unpack_nats_msg(bmsg)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
log.warning(self.fmt(f"Error unpacking message: {e}"))
|
|
164
|
+
# Mesaage malformed - non recoverable error (for this message)
|
|
165
|
+
raise self.ErrorException('pop', e)
|
|
166
|
+
|
|
158
167
|
try:
|
|
159
168
|
if self.reader.last_seq is not None and meta['nats']['seq'] <= self.reader.last_seq: # dupes can happen on reopen
|
|
160
169
|
log.info(self.fmt(f"Skipping duplicated message seq={meta['nats']['seq']}, last_seq={self.reader.last_seq}"))
|
|
161
170
|
raise self.ContinueException('pop')
|
|
162
171
|
except self.ContinueException:
|
|
172
|
+
# Duplicate removed from queue - recovered from a problem
|
|
163
173
|
raise
|
|
164
174
|
except Exception as e:
|
|
175
|
+
# Error checking sequence, can be ignored
|
|
165
176
|
log.warning(self.fmt(f"Error checking seq: {e}"))
|
|
166
177
|
meta['receive_mode'] = 'pull'
|
|
167
178
|
try: # nonessential
|
|
@@ -170,36 +181,52 @@ class MsgReader(MsgDriver):
|
|
|
170
181
|
if len(self.reader.messages) == 0:
|
|
171
182
|
self.reader._emptied.set()
|
|
172
183
|
except Exception as e:
|
|
173
|
-
|
|
184
|
+
# Error in logging, can be ignored
|
|
185
|
+
log.warning(self.fmt(f"Error posprocessing message: {e}"))
|
|
174
186
|
# log.info(self.fmt(f"Returning message seq={meta['nats']['seq']}, last_seq={self.reader.last_seq}"))
|
|
175
187
|
raise self.ReturnException('pop', data, meta)
|
|
188
|
+
|
|
176
189
|
@async_shield
|
|
177
190
|
async def ensure_open(self) -> None:
|
|
178
191
|
if not self.reader.is_open:
|
|
179
192
|
if self.reader._expect_beeing_open:
|
|
180
193
|
log.warning(self.fmt("The reader is not open, but once it has been, trying to reopen"))
|
|
181
|
-
await self.reader.open()
|
|
194
|
+
await self.reader.open() # exceptions handled by the async_shield decorator
|
|
182
195
|
if self.reader.is_open:
|
|
183
196
|
self.reader._expect_beeing_open = True
|
|
197
|
+
# Recover from a problem
|
|
184
198
|
raise self.ContinueException('open')
|
|
185
199
|
else:
|
|
186
|
-
raise Exception('Opened, but still close')
|
|
200
|
+
raise Exception('Opened, but still close') # exception handled by the async_shield decorator
|
|
201
|
+
|
|
187
202
|
@async_shield
|
|
188
203
|
async def ensure_not_stopped(self) -> None:
|
|
189
204
|
if self.reader._stop.is_set():
|
|
190
|
-
|
|
205
|
+
try:
|
|
206
|
+
await self.reader.close()
|
|
207
|
+
except Exception as e:
|
|
208
|
+
# Error in closing can be ignored we are finishing iteration anyway
|
|
209
|
+
log.warning(self.fmt(f"Error closing reader: {e}"))
|
|
191
210
|
raise self.EndIterationException('stop')
|
|
211
|
+
|
|
192
212
|
@async_shield
|
|
193
213
|
async def ensure_consumer(self) -> None:
|
|
194
214
|
if self.error is not None:
|
|
195
215
|
try:
|
|
196
216
|
ci = await self.reader.pull_subscription.consumer_info()
|
|
197
|
-
|
|
217
|
+
# Consumer exists and is accessible
|
|
218
|
+
log.debug(self.fmt(f"Consumer check OK: {ci.name}"))
|
|
198
219
|
except nats.js.errors.NotFoundError:
|
|
199
220
|
log.warning(self.fmt("Consumer has gone, trying to recreate it"))
|
|
200
221
|
await self.reader._reopen()
|
|
201
222
|
log.info(self.fmt(f"Consumer re-opened"))
|
|
202
223
|
raise self.ContinueException('reopen')
|
|
224
|
+
except Exception as e:
|
|
225
|
+
log.warning(self.fmt(f"Error checking consumer, will try to recreate anyway: {e}"))
|
|
226
|
+
await self.reader._reopen()
|
|
227
|
+
log.info(self.fmt(f"Consumer looks like re-opened after error"))
|
|
228
|
+
raise self.ContinueException('reopen')
|
|
229
|
+
|
|
203
230
|
@async_shield
|
|
204
231
|
async def read_batch(self) -> None:
|
|
205
232
|
if len(self.reader.messages) == 0:
|
|
@@ -242,6 +269,15 @@ class MsgReader(MsgDriver):
|
|
|
242
269
|
# The loop which tries hard to get some data
|
|
243
270
|
while True:
|
|
244
271
|
try:
|
|
272
|
+
# If the step method:
|
|
273
|
+
# - retuns data - it will raiseReturnException
|
|
274
|
+
# - corrected something - it will raise ContinueException
|
|
275
|
+
# - wants stop iteration - it will raise EndIterationException
|
|
276
|
+
# - encounters an unrecoverable error - it will raise ErrorException
|
|
277
|
+
# - did not do anything - it will return and next step will be called
|
|
278
|
+
# Those mthod should not raise any other exceptions, if they do, it is a bug,
|
|
279
|
+
# this is enforced by @async_shield (conversion of nay exception to ErrorException)
|
|
280
|
+
|
|
245
281
|
# 0. Do we have some data to return already?
|
|
246
282
|
await st.pop_msg()
|
|
247
283
|
# 1. Check if iteration have been stopped externally
|
|
@@ -281,11 +317,12 @@ class MsgReader(MsgDriver):
|
|
|
281
317
|
log.warning(st.fmt(f"read_next error, (retry in {wait_time:.1f}s): {e.error}"))
|
|
282
318
|
await asyncio.sleep(wait_time)
|
|
283
319
|
case _: # should not be reached
|
|
284
|
-
log.error(st.fmt(f"Invalid on_connection_close value {self.error_behavior}"
|
|
285
|
-
|
|
320
|
+
log.error(st.fmt(f"Invalid on_connection_close value {self.error_behavior}. "
|
|
321
|
+
f"Raport this as a bug to the serverish maintainers!"))
|
|
322
|
+
exit(-107) # this is not i/o error but programming error
|
|
286
323
|
except Exception as e:# should never happen
|
|
287
|
-
log.error(st.fmt(f"
|
|
288
|
-
exit(-
|
|
324
|
+
log.error(st.fmt(f"Unhandled exception {e}. Raport this as a bug to the serverish maintainers!"))
|
|
325
|
+
exit(-108) # this is not i/o error but programming error
|
|
289
326
|
st.n += 1
|
|
290
327
|
# end while
|
|
291
328
|
|
|
@@ -406,7 +443,7 @@ class MsgReader(MsgDriver):
|
|
|
406
443
|
|
|
407
444
|
async def open(self) -> None:
|
|
408
445
|
if self.pull_subscription is not None:
|
|
409
|
-
raise
|
|
446
|
+
raise MessengerReaderAlreadyOpen("Reader already open, do not reuse MsgReader instances")
|
|
410
447
|
|
|
411
448
|
log.debug(f"Opening {self}")
|
|
412
449
|
js = self.connection.js
|
|
@@ -433,25 +470,52 @@ class MsgReader(MsgDriver):
|
|
|
433
470
|
async def _create_pull_subscribtion(self, consumer_conf: ConsumerConfig):
|
|
434
471
|
# Durable consumer is probably not needed (at least problematic)
|
|
435
472
|
# consumer_conf.durable_name = self.name if consumer_conf.durable_name is None else consumer_conf.durable_name
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
473
|
+
if self.connection is None or self.connection.js is None:
|
|
474
|
+
raise MessengerNotConnected('Cannot create pull subscription, not connected to NATS')
|
|
475
|
+
try:
|
|
476
|
+
ret = await self.connection.js.pull_subscribe(self.subject,
|
|
477
|
+
durable=consumer_conf.durable_name,
|
|
478
|
+
config=consumer_conf)
|
|
479
|
+
except TimeoutError as e:
|
|
480
|
+
raise MessengerRequestTimeout(e)
|
|
439
481
|
ci = await ret.consumer_info()
|
|
440
482
|
return ret
|
|
441
483
|
|
|
442
484
|
async def _reopen(self) -> None:
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
485
|
+
"""Reopens the pull subscription with retry logic and better error handling"""
|
|
486
|
+
max_retries = 3
|
|
487
|
+
for attempt in range(max_retries):
|
|
488
|
+
try:
|
|
489
|
+
# Clean up existing subscription
|
|
490
|
+
if self.pull_subscription is not None:
|
|
491
|
+
try:
|
|
492
|
+
await self.pull_subscription.unsubscribe()
|
|
493
|
+
except Exception as e:
|
|
494
|
+
log.debug(f"Error unsubscribing during reopen attempt {attempt + 1}: {e}")
|
|
495
|
+
|
|
496
|
+
# Create new consumer configuration
|
|
497
|
+
consumer_conf = await self._create_consumer_cfg()
|
|
498
|
+
|
|
499
|
+
# Set policy for 'new messages' if any message was received
|
|
500
|
+
if self.last_seq is not None:
|
|
501
|
+
log.info(f"Reopening {self}, from seq={self.last_seq + 1}")
|
|
502
|
+
consumer_conf.deliver_policy = DeliverPolicy.BY_START_SEQUENCE
|
|
503
|
+
consumer_conf.opt_start_time = None
|
|
504
|
+
consumer_conf.opt_start_seq = self.last_seq + 1
|
|
505
|
+
|
|
506
|
+
# Create new subscription
|
|
507
|
+
self.pull_subscription = await self._create_pull_subscribtion(consumer_conf=consumer_conf)
|
|
508
|
+
log.info(f"Successfully reopened {self} after {attempt + 1} attempts")
|
|
509
|
+
return # Success!
|
|
510
|
+
|
|
511
|
+
except Exception as e:
|
|
512
|
+
if attempt == max_retries - 1:
|
|
513
|
+
log.error(f"Failed to reopen {self} after {max_retries} attempts: {e}")
|
|
514
|
+
raise
|
|
515
|
+
|
|
516
|
+
wait_time = (attempt + 1) * 2 # Exponential backoff: 2s, 4s, 6s
|
|
517
|
+
log.warning(f"Reopen attempt {attempt + 1} failed, retrying in {wait_time}s: {e}")
|
|
518
|
+
await asyncio.sleep(wait_time)
|
|
455
519
|
|
|
456
520
|
|
|
457
521
|
|
|
@@ -479,7 +543,7 @@ class MsgReader(MsgDriver):
|
|
|
479
543
|
return consumer_conf
|
|
480
544
|
|
|
481
545
|
async def close(self) -> None:
|
|
482
|
-
|
|
546
|
+
self.connection.remove_reconnect_cb(self.on_nats_reconnect)
|
|
483
547
|
await super().close()
|
|
484
548
|
await self._close_pull_subscription()
|
|
485
549
|
await self._close_push_subscription()
|
|
@@ -58,7 +58,7 @@ class MsgRpcResponder(MsgDriver):
|
|
|
58
58
|
rpc.set_response(data={'c': c})
|
|
59
59
|
|
|
60
60
|
responder = MsgRpcResponder(subject='subject')
|
|
61
|
-
responder.open()
|
|
61
|
+
await responder.open()
|
|
62
62
|
try:
|
|
63
63
|
await responder.register_function(callback)
|
|
64
64
|
# ... wait for incoming messages
|
|
@@ -135,7 +135,7 @@ def get_rpcresponder(subject: str) -> 'MsgRpcResponder':
|
|
|
135
135
|
rpc.set_response(data={'c': c})
|
|
136
136
|
|
|
137
137
|
responder = MsgRpcResponder(subject='subject')
|
|
138
|
-
responder.open()
|
|
138
|
+
await responder.open()
|
|
139
139
|
try:
|
|
140
140
|
await responder.register_function(callback)
|
|
141
141
|
# ... wait for incoming messages
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
import asyncio
|
|
5
5
|
from asyncio import Event
|
|
6
|
+
from time import time
|
|
6
7
|
|
|
7
8
|
import nats.errors
|
|
8
9
|
import param
|
|
@@ -13,6 +14,7 @@ from nats.js.api import DeliverPolicy, ConsumerConfig
|
|
|
13
14
|
from serverish.messenger import Messenger
|
|
14
15
|
from serverish.messenger.messenger import MsgDriver
|
|
15
16
|
from serverish.messenger.msg_reader import MsgReader
|
|
17
|
+
from serverish.base.exceptions import MessengerReaderAlreadyOpen
|
|
16
18
|
|
|
17
19
|
log = logging.getLogger(__name__.rsplit('.')[-1])
|
|
18
20
|
|
|
@@ -37,7 +39,27 @@ class MsgSingleReader(MsgReader):
|
|
|
37
39
|
tuple: data, meta
|
|
38
40
|
|
|
39
41
|
"""
|
|
40
|
-
|
|
42
|
+
interval = 0.1
|
|
43
|
+
max_delay = 5.0
|
|
44
|
+
# abolute utc timeout for waiting for data (now (utc) + wait):
|
|
45
|
+
timeout = time() + wait if wait is not None else None
|
|
46
|
+
|
|
47
|
+
while True:
|
|
48
|
+
try:
|
|
49
|
+
await self.open()
|
|
50
|
+
break
|
|
51
|
+
except MessengerReaderAlreadyOpen:
|
|
52
|
+
break
|
|
53
|
+
except Exception as e: # pragma: no cover
|
|
54
|
+
now = time()
|
|
55
|
+
if timeout is None or now > timeout:
|
|
56
|
+
log.error(f"Failed to open single reader for subject {self.subject}: {e}")
|
|
57
|
+
raise e
|
|
58
|
+
log.warning(f"Failed to open single reader for subject {self.subject}: {e}, retrying in {interval} seconds")
|
|
59
|
+
remaining = timeout - time() if timeout is not None else None
|
|
60
|
+
await asyncio.sleep(min(interval, remaining, max_delay) if remaining is not None else min(interval, max_delay))
|
|
61
|
+
interval *= 2
|
|
62
|
+
|
|
41
63
|
try:
|
|
42
64
|
data, meta = await asyncio.wait_for(self.read_next(), timeout=wait)
|
|
43
65
|
finally:
|
|
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
|