serverish 1.2.0__tar.gz → 1.2.1__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.0 → serverish-1.2.1}/PKG-INFO +1 -1
- {serverish-1.2.0 → serverish-1.2.1}/pyproject.toml +1 -1
- {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/messenger.py +1 -1
- {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_reader.py +98 -54
- {serverish-1.2.0 → serverish-1.2.1}/LICENSE +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/README.md +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/__init__.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/base/__init__.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/base/asyncio_util_functions.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/base/collector.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/base/datetime.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/base/exceptions.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/base/fifoset.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/base/hasstatuses.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/base/idmanger.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/base/manageable.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/base/singleton.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/base/status.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/base/task_manager.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/connection/__init__.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/connection/connection.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/connection/connection_jets.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/connection/connection_nats.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/__init__.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/logging.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_callback_sub.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_journal_pub.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_journal_read.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_progress_pub.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_progress_read.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_publisher.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_rpc_req.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_rpc_resp.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_single_pub.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_single_read.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msgvalidator.py +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/config.schema.json +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/default.schema.json +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/include/timestamp.schema.json +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/journal.schema.json +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/meta.schema.json +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/program_current_state.schema.json +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/program_state.schema.json +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/progress.schema.json +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/telemetry.schema.json +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/services/TODO.txt +0 -0
- {serverish-1.2.0 → serverish-1.2.1}/serverish/slog.py +0 -0
|
@@ -338,7 +338,7 @@ class Messenger(Singleton):
|
|
|
338
338
|
Args:
|
|
339
339
|
subject (str): subject to subscribe to
|
|
340
340
|
deliver_policy (str): deliver policy, one of 'all', 'last', 'new', 'by_start_time', will be passed to consumer config
|
|
341
|
-
opt_start_time (datetime): start time for 'by_start_time' deliver policy, will be passed to consumer config
|
|
341
|
+
opt_start_time (datetime): start time for 'by_start_time' deliver policy, will be passed to consumer config, must carry TZ info
|
|
342
342
|
kwargs: additional arguments to pass to the reader and underlying NATS consumer config
|
|
343
343
|
|
|
344
344
|
Returns:
|
|
@@ -4,10 +4,12 @@ import logging
|
|
|
4
4
|
import asyncio
|
|
5
5
|
from asyncio import Event
|
|
6
6
|
from collections import deque
|
|
7
|
-
from datetime import datetime
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
8
|
import time
|
|
9
9
|
from dataclasses import dataclass, field
|
|
10
10
|
import functools
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
import nats.errors
|
|
13
15
|
import param
|
|
@@ -153,6 +155,14 @@ class MsgReader(MsgDriver):
|
|
|
153
155
|
except Exception as e:
|
|
154
156
|
log.warning(self.fmt(f"Error acking message: {e}"))
|
|
155
157
|
data, meta = self.reader.messenger.unpack_nats_msg(bmsg)
|
|
158
|
+
try:
|
|
159
|
+
if self.reader.last_seq is not None and meta['nats']['seq'] <= self.reader.last_seq: # dupes can happen on reopen
|
|
160
|
+
log.info(self.fmt(f"Skipping duplicated message seq={meta['nats']['seq']}, last_seq={self.reader.last_seq}"))
|
|
161
|
+
raise self.ContinueException('pop')
|
|
162
|
+
except self.ContinueException:
|
|
163
|
+
raise
|
|
164
|
+
except Exception as e:
|
|
165
|
+
log.warning(self.fmt(f"Error checking seq: {e}"))
|
|
156
166
|
meta['receive_mode'] = 'pull'
|
|
157
167
|
try: # nonessential
|
|
158
168
|
self.reader.messenger.log_msg_trace(data, meta, f"SUB PULL iteration from {self.reader.subject}")
|
|
@@ -161,6 +171,7 @@ class MsgReader(MsgDriver):
|
|
|
161
171
|
self.reader._emptied.set()
|
|
162
172
|
except Exception as e:
|
|
163
173
|
log.warning(self.fmt(f"Error unpacking message: {e}"))
|
|
174
|
+
# log.info(self.fmt(f"Returning message seq={meta['nats']['seq']}, last_seq={self.reader.last_seq}"))
|
|
164
175
|
raise self.ReturnException('pop', data, meta)
|
|
165
176
|
@async_shield
|
|
166
177
|
async def ensure_open(self) -> None:
|
|
@@ -192,12 +203,29 @@ class MsgReader(MsgDriver):
|
|
|
192
203
|
@async_shield
|
|
193
204
|
async def read_batch(self) -> None:
|
|
194
205
|
if len(self.reader.messages) == 0:
|
|
195
|
-
timeout = min(0.1 + self.n/10.0, 5.0)
|
|
206
|
+
timeout = 100.0 # min(0.1 + self.n/10.0, 5.0)
|
|
196
207
|
# batch = 1 if self.error is not None else 100 # recover slowly
|
|
197
|
-
log.
|
|
208
|
+
log.info(self.fmt(f"Pulling {self.reader.batch} messages with timeout {timeout}s"))
|
|
198
209
|
new_msgs = await self.reader.fetch_available(batch=self.reader.batch, timeout=timeout)
|
|
199
|
-
log.
|
|
210
|
+
log.info(self.fmt(f"Pulled {len(new_msgs)} messages"))
|
|
211
|
+
|
|
212
|
+
# If no messages were available immediately, switch to blocking mode
|
|
213
|
+
# to wait for at least one message more efficiently
|
|
214
|
+
if len(new_msgs) == 0:
|
|
215
|
+
log.info(self.fmt(f"No messages available immediately, waiting for at least one message with timeout {timeout:0.1f}s"))
|
|
216
|
+
|
|
217
|
+
# Use the regular fetch operation from nats (which blocks)
|
|
218
|
+
try:
|
|
219
|
+
new_msgs = await self.reader.pull_subscription.fetch(1, timeout=timeout)
|
|
220
|
+
log.info(self.fmt(f"Received {len(new_msgs)} message while blocking waiting"))
|
|
221
|
+
except asyncio.TimeoutError:
|
|
222
|
+
log.info(self.fmt(f"No meesge before timeout"))
|
|
223
|
+
except Exception as e:
|
|
224
|
+
log.warning(self.fmt(f"Error waiting for messages: {e}"))
|
|
225
|
+
# fi : no messages available immediately
|
|
226
|
+
|
|
200
227
|
self.reader.messages.extend(new_msgs)
|
|
228
|
+
|
|
201
229
|
raise self.ContinueException('read')
|
|
202
230
|
|
|
203
231
|
def fmt(self, msg: str) -> str:
|
|
@@ -263,7 +291,7 @@ class MsgReader(MsgDriver):
|
|
|
263
291
|
|
|
264
292
|
|
|
265
293
|
async def fetch_available(self, batch=10, timeout = 0.1):
|
|
266
|
-
"""Fetch only immediately available messages without blocking
|
|
294
|
+
"""[NATS fix] Fetch only immediately available messages without blocking
|
|
267
295
|
|
|
268
296
|
Path on lack of functionality of JetStreamContext.PullSubscription.fetch
|
|
269
297
|
By default uses only very short timeout to account for network latency
|
|
@@ -309,7 +337,7 @@ class MsgReader(MsgDriver):
|
|
|
309
337
|
break
|
|
310
338
|
|
|
311
339
|
try:
|
|
312
|
-
msg = await
|
|
340
|
+
msg = await self.next_msg(timeout=0.0) # nonblocking here
|
|
313
341
|
status = JetStreamContext.is_status_msg(msg)
|
|
314
342
|
if not status:
|
|
315
343
|
msgs.append(msg)
|
|
@@ -318,9 +346,64 @@ class MsgReader(MsgDriver):
|
|
|
318
346
|
break
|
|
319
347
|
except asyncio.TimeoutError:
|
|
320
348
|
break
|
|
349
|
+
except asyncio.QueueEmpty:
|
|
350
|
+
log.info(f"{self} Queue is empty")
|
|
351
|
+
break
|
|
321
352
|
|
|
322
353
|
return msgs
|
|
323
354
|
|
|
355
|
+
async def next_msg(self, timeout: Optional[float] = None) -> Msg:
|
|
356
|
+
""" [NATS fix] Fetch the next message from the subscription
|
|
357
|
+
:params timeout: Time in seconds to wait for next message before timing out.
|
|
358
|
+
supports 0 for non-blocking and None for blocking indefinitely.
|
|
359
|
+
:raises nats.errors.TimeoutError:
|
|
360
|
+
|
|
361
|
+
next_msg can be used to retrieve the next message from a stream of messages using
|
|
362
|
+
await syntax, this only works when not passing a callback on `subscribe`::
|
|
363
|
+
|
|
364
|
+
sub = await nc.subscribe('hello')
|
|
365
|
+
msg = await sub.next_msg(timeout=1)
|
|
366
|
+
|
|
367
|
+
"""
|
|
368
|
+
ps = self.pull_subscription._sub
|
|
369
|
+
if ps._conn.is_closed:
|
|
370
|
+
raise errors.ConnectionClosedError
|
|
371
|
+
|
|
372
|
+
if ps._cb:
|
|
373
|
+
raise errors.Error(
|
|
374
|
+
'nats: next_msg cannot be used in async subscriptions'
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
task_name = str(uuid4())
|
|
378
|
+
try:
|
|
379
|
+
if timeout == 0.0:
|
|
380
|
+
future = asyncio.create_task(
|
|
381
|
+
ps._pending_queue.get_nowait()
|
|
382
|
+
)
|
|
383
|
+
else:
|
|
384
|
+
future = asyncio.create_task(
|
|
385
|
+
asyncio.wait_for(ps._pending_queue.get(), timeout)
|
|
386
|
+
)
|
|
387
|
+
ps._pending_next_msgs_calls[task_name] = future
|
|
388
|
+
msg = await future
|
|
389
|
+
except asyncio.TimeoutError:
|
|
390
|
+
if ps._conn.is_closed:
|
|
391
|
+
raise errors.ConnectionClosedError
|
|
392
|
+
raise errors.TimeoutError
|
|
393
|
+
except asyncio.CancelledError:
|
|
394
|
+
if ps._conn.is_closed:
|
|
395
|
+
raise errors.ConnectionClosedError
|
|
396
|
+
raise
|
|
397
|
+
else:
|
|
398
|
+
ps._pending_size -= len(msg.data)
|
|
399
|
+
# For sync subscriptions we will consider a message
|
|
400
|
+
# to be done once it has been consumed by the client
|
|
401
|
+
# regardless of whether it has been processed.
|
|
402
|
+
ps._pending_queue.task_done()
|
|
403
|
+
return msg
|
|
404
|
+
finally:
|
|
405
|
+
ps._pending_next_msgs_calls.pop(task_name, None)
|
|
406
|
+
|
|
324
407
|
async def open(self) -> None:
|
|
325
408
|
if self.pull_subscription is not None:
|
|
326
409
|
raise RuntimeError("Reader already open, do not reuse MsgReader instances")
|
|
@@ -364,6 +447,7 @@ class MsgReader(MsgDriver):
|
|
|
364
447
|
consumer_conf = await self._create_consumer_cfg()
|
|
365
448
|
# set policy for 'new messages' for BY_START_SEQUENCE if any message was received
|
|
366
449
|
if self.last_seq is not None:
|
|
450
|
+
log.info(f"Reopening {self}, from seq={self.last_seq + 1}")
|
|
367
451
|
consumer_conf.deliver_policy = DeliverPolicy.BY_START_SEQUENCE
|
|
368
452
|
consumer_conf.opt_start_time = None
|
|
369
453
|
consumer_conf.opt_start_seq = self.last_seq + 1
|
|
@@ -371,52 +455,6 @@ class MsgReader(MsgDriver):
|
|
|
371
455
|
|
|
372
456
|
|
|
373
457
|
|
|
374
|
-
async def _transform_to_pull_sub(self):
|
|
375
|
-
log.info(f"Attempt to transform {self.subject} MeassageReader mode to PULL (from seq: {self.last_seq})")
|
|
376
|
-
try:
|
|
377
|
-
await self.push_subscription.unsubscribe()
|
|
378
|
-
except (nats.errors.ConnectionClosedError, AttributeError): # no connection, no reason to break
|
|
379
|
-
pass
|
|
380
|
-
self.push_subscription = None
|
|
381
|
-
self._emptied.clear()
|
|
382
|
-
consumer_conf = await self._create_consumer_cfg()
|
|
383
|
-
# set policy for 'new messages' for push subscription
|
|
384
|
-
if self.last_seq is not None:
|
|
385
|
-
consumer_conf.deliver_policy = DeliverPolicy.BY_START_SEQUENCE
|
|
386
|
-
consumer_conf.opt_start_time = None
|
|
387
|
-
consumer_conf.opt_start_seq = self.last_seq + 1
|
|
388
|
-
else:
|
|
389
|
-
consumer_conf.deliver_policy = DeliverPolicy.NEW
|
|
390
|
-
consumer_conf.opt_start_time = None
|
|
391
|
-
consumer_conf.opt_start_seq = None
|
|
392
|
-
self.pull_subscription = await self._create_pull_subscribtion(consumer_conf=consumer_conf)
|
|
393
|
-
log.info(f"{self}: PULL subscription set up "
|
|
394
|
-
f"(policy={consumer_conf.deliver_policy}, seq={consumer_conf.opt_start_seq})")
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
async def _transform_to_push_sub(self):
|
|
399
|
-
"""After fetching existing messages, switch to push based consumer"""
|
|
400
|
-
log.info(f"Attempt to transform {self.subject} MeassageReader mode to PUSH for future messages")
|
|
401
|
-
pull = self.pull_subscription
|
|
402
|
-
if pull is None: # no transition needed or not possible
|
|
403
|
-
log.debug(f"No pull - no transform {self}")
|
|
404
|
-
return
|
|
405
|
-
|
|
406
|
-
consumer_conf = await self._create_consumer_cfg()
|
|
407
|
-
# set policy for 'new messages' for push subscription
|
|
408
|
-
consumer_conf.deliver_policy = DeliverPolicy.NEW
|
|
409
|
-
consumer_conf.opt_start_time = None
|
|
410
|
-
consumer_conf.opt_start_seq = None
|
|
411
|
-
js = self.connection.js
|
|
412
|
-
|
|
413
|
-
push: JetStreamContext.PushSubscription = await js.subscribe(
|
|
414
|
-
self.subject, config=consumer_conf
|
|
415
|
-
)
|
|
416
|
-
self.push_subscription = push
|
|
417
|
-
log.info(f"{self}: PUSH subscription set up")
|
|
418
|
-
# self._push.set()
|
|
419
|
-
|
|
420
458
|
async def _create_consumer_cfg(self) -> ConsumerConfig:
|
|
421
459
|
cfg = self.consumer_cfg.copy()
|
|
422
460
|
# Convert the delivery policy from a string to the appropriate DeliverPolicy enum
|
|
@@ -427,8 +465,14 @@ class MsgReader(MsgDriver):
|
|
|
427
465
|
if isinstance(self.opt_start_time, str):
|
|
428
466
|
cfg['opt_start_time'] = self.opt_start_time
|
|
429
467
|
else:
|
|
430
|
-
#
|
|
431
|
-
|
|
468
|
+
# Check if timezone is set, if not warn
|
|
469
|
+
if self.opt_start_time.tzinfo is None:
|
|
470
|
+
log.warning(f"opt_start_time should have timezone information, converting to UTC: "
|
|
471
|
+
f"{self.opt_start_time.strftime('%Y-%m-%dT%H:%M:%S.%f')}"
|
|
472
|
+
f"=>{self.opt_start_time.astimezone(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%fZ')}"
|
|
473
|
+
f" , use e.g. `datetime.now(tz=timezone.utc)`")
|
|
474
|
+
|
|
475
|
+
cfg['opt_start_time'] = self.opt_start_time.astimezone(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
|
432
476
|
|
|
433
477
|
# Create the pull consumer configuration
|
|
434
478
|
consumer_conf = ConsumerConfig(**cfg)
|
|
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
|