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.
Files changed (48) hide show
  1. {serverish-1.2.2 → serverish-1.4.0}/PKG-INFO +4 -5
  2. {serverish-1.2.2 → serverish-1.4.0}/README.md +2 -1
  3. {serverish-1.2.2 → serverish-1.4.0}/pyproject.toml +2 -2
  4. {serverish-1.2.2 → serverish-1.4.0}/serverish/base/exceptions.py +5 -1
  5. serverish-1.4.0/serverish/base/iterators.py +97 -0
  6. {serverish-1.2.2 → serverish-1.4.0}/serverish/connection/connection_jets.py +8 -3
  7. {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_reader.py +92 -28
  8. {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_rpc_resp.py +2 -2
  9. {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_single_read.py +23 -1
  10. {serverish-1.2.2 → serverish-1.4.0}/LICENSE +0 -0
  11. {serverish-1.2.2 → serverish-1.4.0}/serverish/__init__.py +0 -0
  12. {serverish-1.2.2 → serverish-1.4.0}/serverish/base/__init__.py +0 -0
  13. {serverish-1.2.2 → serverish-1.4.0}/serverish/base/asyncio_util_functions.py +0 -0
  14. {serverish-1.2.2 → serverish-1.4.0}/serverish/base/collector.py +0 -0
  15. {serverish-1.2.2 → serverish-1.4.0}/serverish/base/datetime.py +0 -0
  16. {serverish-1.2.2 → serverish-1.4.0}/serverish/base/fifoset.py +0 -0
  17. {serverish-1.2.2 → serverish-1.4.0}/serverish/base/hasstatuses.py +0 -0
  18. {serverish-1.2.2 → serverish-1.4.0}/serverish/base/idmanger.py +0 -0
  19. {serverish-1.2.2 → serverish-1.4.0}/serverish/base/manageable.py +0 -0
  20. {serverish-1.2.2 → serverish-1.4.0}/serverish/base/singleton.py +0 -0
  21. {serverish-1.2.2 → serverish-1.4.0}/serverish/base/status.py +0 -0
  22. {serverish-1.2.2 → serverish-1.4.0}/serverish/base/task_manager.py +0 -0
  23. {serverish-1.2.2 → serverish-1.4.0}/serverish/connection/__init__.py +0 -0
  24. {serverish-1.2.2 → serverish-1.4.0}/serverish/connection/connection.py +0 -0
  25. {serverish-1.2.2 → serverish-1.4.0}/serverish/connection/connection_nats.py +0 -0
  26. {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/__init__.py +0 -0
  27. {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/logging.py +0 -0
  28. {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/messenger.py +0 -0
  29. {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_callback_sub.py +0 -0
  30. {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_journal_pub.py +0 -0
  31. {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_journal_read.py +0 -0
  32. {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_progress_pub.py +0 -0
  33. {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_progress_read.py +0 -0
  34. {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_publisher.py +0 -0
  35. {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_rpc_req.py +0 -0
  36. {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msg_single_pub.py +0 -0
  37. {serverish-1.2.2 → serverish-1.4.0}/serverish/messenger/msgvalidator.py +0 -0
  38. {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/config.schema.json +0 -0
  39. {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/default.schema.json +0 -0
  40. {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/include/timestamp.schema.json +0 -0
  41. {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/journal.schema.json +0 -0
  42. {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/meta.schema.json +0 -0
  43. {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/program_current_state.schema.json +0 -0
  44. {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/program_state.schema.json +0 -0
  45. {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/progress.schema.json +0 -0
  46. {serverish-1.2.2 → serverish-1.4.0}/serverish/schema/telemetry.schema.json +0 -0
  47. {serverish-1.2.2 → serverish-1.4.0}/serverish/services/TODO.txt +0 -0
  48. {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.2.2
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.8,<4.0
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
- Following extras are available:
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
- Following extras are available:
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.2.2"
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.8"
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
- self.setup_jetstream()
93
- await self.update_statuses()
94
- logging.info(f'Jeststream reconnect status: {self.format_status()}')
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
- data, meta = self.reader.messenger.unpack_nats_msg(bmsg)
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
- log.warning(self.fmt(f"Error unpacking message: {e}"))
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
- await self.reader.close()
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
- print(ci)
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
- exit(-1) # this is not i/o error but programming error
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"unhandled exception {e}"))
288
- exit(-1) # this is not i/o error but programming error
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 RuntimeError("Reader already open, do not reuse MsgReader instances")
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
- ret = await self.connection.js.pull_subscribe(self.subject,
437
- durable=consumer_conf.durable_name,
438
- config=consumer_conf)
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
- try:
444
- await self.pull_subscription.unsubscribe()
445
- except Exception:
446
- pass
447
- consumer_conf = await self._create_consumer_cfg()
448
- # set policy for 'new messages' for BY_START_SEQUENCE if any message was received
449
- if self.last_seq is not None:
450
- log.info(f"Reopening {self}, from seq={self.last_seq + 1}")
451
- consumer_conf.deliver_policy = DeliverPolicy.BY_START_SEQUENCE
452
- consumer_conf.opt_start_time = None
453
- consumer_conf.opt_start_seq = self.last_seq + 1
454
- self.pull_subscription = await self._create_pull_subscribtion(consumer_conf=consumer_conf)
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
- Messenger().connection.remove_reconnect_cb(self.on_nats_reconnect)
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
- await self.open()
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