serverish 1.4.2__tar.gz → 1.4.3__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.4.2 → serverish-1.4.3}/PKG-INFO +1 -1
- {serverish-1.4.2 → serverish-1.4.3}/pyproject.toml +1 -1
- {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/messenger.py +17 -2
- {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_reader.py +96 -30
- {serverish-1.4.2 → serverish-1.4.3}/LICENSE +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/README.md +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/__init__.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/base/__init__.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/base/asyncio_util_functions.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/base/collector.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/base/datetime.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/base/exceptions.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/base/fifoset.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/base/hasstatuses.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/base/idmanger.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/base/iterators.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/base/manageable.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/base/singleton.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/base/status.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/base/task_manager.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/connection/__init__.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/connection/connection.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/connection/connection_jets.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/connection/connection_nats.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/__init__.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/logging.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_callback_sub.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_journal_pub.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_journal_read.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_progress_pub.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_progress_read.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_publisher.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_rpc_req.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_rpc_resp.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_single_pub.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_single_read.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msgvalidator.py +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/config.schema.json +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/default.schema.json +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/include/timestamp.schema.json +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/journal.schema.json +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/meta.schema.json +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/program_current_state.schema.json +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/program_state.schema.json +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/progress.schema.json +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/telemetry.schema.json +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/services/TODO.txt +0 -0
- {serverish-1.4.2 → serverish-1.4.3}/serverish/slog.py +0 -0
|
@@ -340,21 +340,36 @@ class Messenger(Singleton):
|
|
|
340
340
|
deliver_policy (str): deliver policy, one of 'all', 'last', 'new', 'by_start_time', will be passed to consumer config
|
|
341
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
|
+
MsgReader-specific params (nowait, error_behavior, on_missed_messages) are extracted
|
|
344
|
+
and passed directly to MsgReader; remaining params go to consumer_cfg
|
|
343
345
|
|
|
344
346
|
Returns:
|
|
345
347
|
MsgReader: message subscriber
|
|
346
348
|
|
|
347
349
|
Usage:
|
|
348
|
-
reader = Messenger.get_reader('subject'):
|
|
350
|
+
reader = Messenger.get_reader('subject', nowait=True):
|
|
349
351
|
async for msg in reader:
|
|
350
352
|
print(msg)
|
|
351
353
|
"""
|
|
352
354
|
from serverish.messenger.msg_reader import MsgReader
|
|
355
|
+
|
|
356
|
+
# Extract MsgReader-specific parameters
|
|
357
|
+
reader_params = {}
|
|
358
|
+
consumer_cfg = {}
|
|
359
|
+
reader_param_names = {'nowait', 'error_behavior', 'on_missed_messages'}
|
|
360
|
+
|
|
361
|
+
for key, value in kwargs.items():
|
|
362
|
+
if key in reader_param_names:
|
|
363
|
+
reader_params[key] = value
|
|
364
|
+
else:
|
|
365
|
+
consumer_cfg[key] = value
|
|
366
|
+
|
|
353
367
|
return MsgReader(subject=subject,
|
|
354
368
|
parent=Messenger(),
|
|
355
369
|
deliver_policy=deliver_policy,
|
|
356
370
|
opt_start_time=opt_start_time,
|
|
357
|
-
consumer_cfg=
|
|
371
|
+
consumer_cfg=consumer_cfg,
|
|
372
|
+
**reader_params)
|
|
358
373
|
|
|
359
374
|
@staticmethod
|
|
360
375
|
def get_singlepublisher(subject):
|
|
@@ -76,7 +76,6 @@ class MsgReader(MsgDriver):
|
|
|
76
76
|
deliver_policy = 'all',
|
|
77
77
|
opt_start_time = None,
|
|
78
78
|
consumer_cfg=None,
|
|
79
|
-
nowait: bool = False,
|
|
80
79
|
**kwargs) -> None:
|
|
81
80
|
if parent is None:
|
|
82
81
|
parent = Messenger()
|
|
@@ -245,30 +244,32 @@ class MsgReader(MsgDriver):
|
|
|
245
244
|
@async_shield
|
|
246
245
|
async def read_batch(self) -> None:
|
|
247
246
|
if len(self.reader.messages) == 0:
|
|
248
|
-
timeout
|
|
249
|
-
#
|
|
250
|
-
|
|
251
|
-
|
|
247
|
+
# For fetch_available, timeout is network latency budget (not message wait time)
|
|
248
|
+
# Server responds immediately with no_wait=True, we just account for slow networks
|
|
249
|
+
fetch_timeout = 2.0 # Reduced - should be quick with no_wait
|
|
250
|
+
log.debug(self.fmt(f"Pulling {self.reader.batch} messages with timeout {fetch_timeout}s"))
|
|
251
|
+
new_msgs = await self.reader.fetch_available(batch=self.reader.batch, timeout=fetch_timeout)
|
|
252
252
|
log.debug(self.fmt(f"Pulled {len(new_msgs)} messages"))
|
|
253
253
|
|
|
254
|
-
# If no messages were available
|
|
255
|
-
# to wait for at least one message more efficiently
|
|
254
|
+
# If no messages were available (got 404 from server or timeout), decide what to do
|
|
256
255
|
if len(new_msgs) == 0:
|
|
257
256
|
if self.reader.nowait:
|
|
258
|
-
|
|
257
|
+
# Server confirmed no messages - finish iteration
|
|
258
|
+
log.debug(self.fmt(f"No messages available, finishing due to nowait"))
|
|
259
259
|
raise self.EndIterationException('nowait')
|
|
260
260
|
|
|
261
|
-
|
|
261
|
+
# Non-nowait mode: wait for NEW messages to arrive
|
|
262
|
+
blocking_timeout = 100.0
|
|
263
|
+
log.debug(self.fmt(f"No messages currently available, waiting for new messages (timeout {blocking_timeout}s)"))
|
|
262
264
|
|
|
263
|
-
# Use the regular fetch operation
|
|
265
|
+
# Use the regular fetch operation which waits for messages to arrive
|
|
264
266
|
try:
|
|
265
|
-
new_msgs = await self.reader.pull_subscription.fetch(1, timeout=
|
|
266
|
-
log.debug(self.fmt(f"Received {len(new_msgs)} message
|
|
267
|
+
new_msgs = await self.reader.pull_subscription.fetch(1, timeout=blocking_timeout)
|
|
268
|
+
log.debug(self.fmt(f"Received {len(new_msgs)} new message(s)"))
|
|
267
269
|
except asyncio.TimeoutError:
|
|
268
|
-
log.debug(self.fmt(f"No
|
|
270
|
+
log.debug(self.fmt(f"No new messages arrived within {blocking_timeout}s"))
|
|
269
271
|
except Exception as e:
|
|
270
272
|
log.warning(self.fmt(f"Error waiting for messages: {e}"))
|
|
271
|
-
# fi : no messages available immediately
|
|
272
273
|
|
|
273
274
|
self.reader.messages.extend(new_msgs)
|
|
274
275
|
|
|
@@ -346,21 +347,30 @@ class MsgReader(MsgDriver):
|
|
|
346
347
|
# end while
|
|
347
348
|
|
|
348
349
|
|
|
349
|
-
async def fetch_available(self, batch=10, timeout
|
|
350
|
-
"""[NATS fix] Fetch only immediately available messages without blocking
|
|
350
|
+
async def fetch_available(self, batch=10, timeout=2.0):
|
|
351
|
+
"""[NATS fix] Fetch only immediately available messages without blocking for new messages
|
|
351
352
|
|
|
352
|
-
|
|
353
|
-
|
|
353
|
+
Uses no_wait=True protocol:
|
|
354
|
+
- Server sends available messages immediately
|
|
355
|
+
- Server sends 404 status if no messages exist
|
|
356
|
+
- We wait for server response (network latency) but not for new messages to arrive
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
batch: Maximum number of messages to fetch
|
|
360
|
+
timeout: Network latency budget (default 2s for local/fast networks)
|
|
361
|
+
NOT a "wait for messages" timeout - server won't wait
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
List of messages that were immediately available on server
|
|
354
365
|
"""
|
|
355
366
|
import json
|
|
356
367
|
|
|
357
368
|
pull_subscription = self.pull_subscription
|
|
358
|
-
# Get from internal queue first
|
|
359
369
|
msgs = []
|
|
360
370
|
needed = batch
|
|
361
371
|
queue = pull_subscription._sub._pending_queue
|
|
362
372
|
|
|
363
|
-
# First
|
|
373
|
+
# First, drain messages already in local queue
|
|
364
374
|
while not queue.empty() and needed > 0:
|
|
365
375
|
try:
|
|
366
376
|
msg = queue.get_nowait()
|
|
@@ -369,14 +379,15 @@ class MsgReader(MsgDriver):
|
|
|
369
379
|
if not status: # Skip status messages
|
|
370
380
|
msgs.append(msg)
|
|
371
381
|
needed -= 1
|
|
382
|
+
queue.task_done()
|
|
372
383
|
except Exception:
|
|
373
384
|
pass
|
|
374
385
|
|
|
375
|
-
# If we
|
|
386
|
+
# If we got enough from queue, return immediately
|
|
376
387
|
if needed == 0:
|
|
377
388
|
return msgs
|
|
378
389
|
|
|
379
|
-
#
|
|
390
|
+
# Send pull request: no_wait=True means server responds immediately
|
|
380
391
|
next_req = {"batch": needed, "no_wait": True}
|
|
381
392
|
await pull_subscription._nc.publish(
|
|
382
393
|
pull_subscription._nms,
|
|
@@ -384,26 +395,74 @@ class MsgReader(MsgDriver):
|
|
|
384
395
|
pull_subscription._deliver,
|
|
385
396
|
)
|
|
386
397
|
|
|
398
|
+
# Wait for server response (messages and/or 404 status)
|
|
399
|
+
# The server will respond quickly because no_wait=True
|
|
400
|
+
# We just need to account for network latency
|
|
387
401
|
start_time = time.monotonic()
|
|
402
|
+
got_404_status = False
|
|
403
|
+
got_any_message = False
|
|
388
404
|
|
|
389
|
-
|
|
390
|
-
while needed > 0:
|
|
405
|
+
while not got_404_status:
|
|
391
406
|
deadline = timeout - (time.monotonic() - start_time)
|
|
392
407
|
if deadline <= 0:
|
|
408
|
+
# Timeout - if we got some messages, that's OK (server might not send 404 after exact batch)
|
|
409
|
+
# If we got nothing, this is a network problem
|
|
410
|
+
if not got_any_message and len(msgs) == 0:
|
|
411
|
+
log.warning(f"{self} Timeout after {timeout}s waiting for server response (network issue?)")
|
|
412
|
+
else:
|
|
413
|
+
log.debug(f"{self} Timeout after getting {len(msgs)} messages (no 404 received)")
|
|
393
414
|
break
|
|
394
415
|
|
|
395
416
|
try:
|
|
396
|
-
|
|
417
|
+
# Wait for server to respond (messages or 404)
|
|
418
|
+
msg = await asyncio.wait_for(queue.get(), timeout=deadline)
|
|
419
|
+
pull_subscription._sub._pending_size -= len(msg.data)
|
|
420
|
+
queue.task_done()
|
|
421
|
+
|
|
397
422
|
status = JetStreamContext.is_status_msg(msg)
|
|
398
|
-
|
|
423
|
+
|
|
424
|
+
if status == "404":
|
|
425
|
+
# Server says: no (more) messages available
|
|
426
|
+
# This is the definitive "done" signal
|
|
427
|
+
log.debug(f"{self} Server confirmed no more messages (404 status)")
|
|
428
|
+
got_404_status = True
|
|
429
|
+
break
|
|
430
|
+
elif status == "408":
|
|
431
|
+
# Request Timeout - server gave us what it had but couldn't fill batch
|
|
432
|
+
# This is effectively "no more messages available right now"
|
|
433
|
+
log.debug(f"{self} Server returned partial batch (408 status - request timeout)")
|
|
434
|
+
got_404_status = True # Treat same as 404 - no more messages available
|
|
435
|
+
break
|
|
436
|
+
elif status:
|
|
437
|
+
# Other status message (shouldn't happen with pull, but handle it)
|
|
438
|
+
log.debug(f"{self} Received status: {status}")
|
|
439
|
+
continue
|
|
440
|
+
else:
|
|
441
|
+
# Real message
|
|
399
442
|
msgs.append(msg)
|
|
443
|
+
got_any_message = True
|
|
400
444
|
needed -= 1
|
|
401
|
-
|
|
402
|
-
|
|
445
|
+
|
|
446
|
+
# If we got all requested messages, give server very short time to send 404
|
|
447
|
+
# but don't wait long - likely there are more messages
|
|
448
|
+
if needed == 0:
|
|
449
|
+
# Very short grace period for 404 status (100ms)
|
|
450
|
+
# If 404 doesn't arrive, we'll just fetch again on next iteration
|
|
451
|
+
remaining_time = timeout - (time.monotonic() - start_time)
|
|
452
|
+
if remaining_time > 0.1:
|
|
453
|
+
deadline = 0.1
|
|
454
|
+
timeout = time.monotonic() - start_time + 0.1 # Update timeout to exit on next deadline check
|
|
455
|
+
|
|
403
456
|
except asyncio.TimeoutError:
|
|
457
|
+
# Timeout after getting messages is OK - server might not send 404 if we got exact batch
|
|
458
|
+
if got_any_message or len(msgs) > 0:
|
|
459
|
+
log.debug(f"{self} Got {len(msgs)} messages, no 404 status (normal for exact batch)")
|
|
460
|
+
else:
|
|
461
|
+
# No messages at all - this could be empty subject or network problem
|
|
462
|
+
log.debug(f"{self} No messages or 404 within {timeout}s")
|
|
404
463
|
break
|
|
405
|
-
except
|
|
406
|
-
log.
|
|
464
|
+
except Exception as e:
|
|
465
|
+
log.warning(f"{self} Error in fetch_available: {e}")
|
|
407
466
|
break
|
|
408
467
|
|
|
409
468
|
return msgs
|
|
@@ -540,6 +599,13 @@ class MsgReader(MsgDriver):
|
|
|
540
599
|
|
|
541
600
|
async def _create_consumer_cfg(self) -> ConsumerConfig:
|
|
542
601
|
cfg = self.consumer_cfg.copy()
|
|
602
|
+
|
|
603
|
+
# Remove MsgReader-specific parameters that are not part of ConsumerConfig
|
|
604
|
+
# These are handled by MsgReader itself, not passed to NATS
|
|
605
|
+
reader_params = ['nowait', 'error_behavior', 'on_missed_messages']
|
|
606
|
+
for param in reader_params:
|
|
607
|
+
cfg.pop(param, None)
|
|
608
|
+
|
|
543
609
|
# Convert the delivery policy from a string to the appropriate DeliverPolicy enum
|
|
544
610
|
if self.deliver_policy is not None:
|
|
545
611
|
cfg['deliver_policy'] = DeliverPolicy(self.deliver_policy)
|
|
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
|