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.
Files changed (48) hide show
  1. {serverish-1.4.2 → serverish-1.4.3}/PKG-INFO +1 -1
  2. {serverish-1.4.2 → serverish-1.4.3}/pyproject.toml +1 -1
  3. {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/messenger.py +17 -2
  4. {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_reader.py +96 -30
  5. {serverish-1.4.2 → serverish-1.4.3}/LICENSE +0 -0
  6. {serverish-1.4.2 → serverish-1.4.3}/README.md +0 -0
  7. {serverish-1.4.2 → serverish-1.4.3}/serverish/__init__.py +0 -0
  8. {serverish-1.4.2 → serverish-1.4.3}/serverish/base/__init__.py +0 -0
  9. {serverish-1.4.2 → serverish-1.4.3}/serverish/base/asyncio_util_functions.py +0 -0
  10. {serverish-1.4.2 → serverish-1.4.3}/serverish/base/collector.py +0 -0
  11. {serverish-1.4.2 → serverish-1.4.3}/serverish/base/datetime.py +0 -0
  12. {serverish-1.4.2 → serverish-1.4.3}/serverish/base/exceptions.py +0 -0
  13. {serverish-1.4.2 → serverish-1.4.3}/serverish/base/fifoset.py +0 -0
  14. {serverish-1.4.2 → serverish-1.4.3}/serverish/base/hasstatuses.py +0 -0
  15. {serverish-1.4.2 → serverish-1.4.3}/serverish/base/idmanger.py +0 -0
  16. {serverish-1.4.2 → serverish-1.4.3}/serverish/base/iterators.py +0 -0
  17. {serverish-1.4.2 → serverish-1.4.3}/serverish/base/manageable.py +0 -0
  18. {serverish-1.4.2 → serverish-1.4.3}/serverish/base/singleton.py +0 -0
  19. {serverish-1.4.2 → serverish-1.4.3}/serverish/base/status.py +0 -0
  20. {serverish-1.4.2 → serverish-1.4.3}/serverish/base/task_manager.py +0 -0
  21. {serverish-1.4.2 → serverish-1.4.3}/serverish/connection/__init__.py +0 -0
  22. {serverish-1.4.2 → serverish-1.4.3}/serverish/connection/connection.py +0 -0
  23. {serverish-1.4.2 → serverish-1.4.3}/serverish/connection/connection_jets.py +0 -0
  24. {serverish-1.4.2 → serverish-1.4.3}/serverish/connection/connection_nats.py +0 -0
  25. {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/__init__.py +0 -0
  26. {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/logging.py +0 -0
  27. {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_callback_sub.py +0 -0
  28. {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_journal_pub.py +0 -0
  29. {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_journal_read.py +0 -0
  30. {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_progress_pub.py +0 -0
  31. {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_progress_read.py +0 -0
  32. {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_publisher.py +0 -0
  33. {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_rpc_req.py +0 -0
  34. {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_rpc_resp.py +0 -0
  35. {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_single_pub.py +0 -0
  36. {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msg_single_read.py +0 -0
  37. {serverish-1.4.2 → serverish-1.4.3}/serverish/messenger/msgvalidator.py +0 -0
  38. {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/config.schema.json +0 -0
  39. {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/default.schema.json +0 -0
  40. {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/include/timestamp.schema.json +0 -0
  41. {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/journal.schema.json +0 -0
  42. {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/meta.schema.json +0 -0
  43. {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/program_current_state.schema.json +0 -0
  44. {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/program_state.schema.json +0 -0
  45. {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/progress.schema.json +0 -0
  46. {serverish-1.4.2 → serverish-1.4.3}/serverish/schema/telemetry.schema.json +0 -0
  47. {serverish-1.4.2 → serverish-1.4.3}/serverish/services/TODO.txt +0 -0
  48. {serverish-1.4.2 → serverish-1.4.3}/serverish/slog.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: serverish
3
- Version: 1.4.2
3
+ Version: 1.4.3
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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "serverish"
3
- version = "1.4.2"
3
+ version = "1.4.3"
4
4
  description = "helpers for server alike projects"
5
5
  authors = ["Mikołaj Kałuszyński", "MMME team"]
6
6
  readme = "README.md"
@@ -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=kwargs)
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 = 100.0 # min(0.1 + self.n/10.0, 5.0)
249
- # batch = 1 if self.error is not None else 100 # recover slowly
250
- log.debug(self.fmt(f"Pulling {self.reader.batch} messages with timeout {timeout}s"))
251
- new_msgs = await self.reader.fetch_available(batch=self.reader.batch, timeout=timeout)
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 immediately, switch to blocking mode
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
- log.debug(self.fmt(f"No new messages found, finishing due to nowait"))
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
- log.debug(self.fmt(f"No messages available immediately, waiting for at least one message with timeout {timeout:0.1f}s"))
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 from nats (which blocks)
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=timeout)
266
- log.debug(self.fmt(f"Received {len(new_msgs)} message while blocking waiting"))
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 meesge before timeout"))
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 = 0.1):
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
- Path on lack of functionality of JetStreamContext.PullSubscription.fetch
353
- By default uses only very short timeout to account for network latency
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 get messages from the internal queue
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 already have enough messages, return early
386
+ # If we got enough from queue, return immediately
376
387
  if needed == 0:
377
388
  return msgs
378
389
 
379
- # Make one no_wait request to get immediately available messages
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
- # Process the response with a very short timeout
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
- msg = await self.next_msg(timeout=0.0) # nonblocking here
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
- if not status:
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
- elif status == "404": # NO_MESSAGES
402
- break
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 asyncio.QueueEmpty:
406
- log.debug(f"{self} Queue is empty")
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