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.
Files changed (47) hide show
  1. {serverish-1.2.0 → serverish-1.2.1}/PKG-INFO +1 -1
  2. {serverish-1.2.0 → serverish-1.2.1}/pyproject.toml +1 -1
  3. {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/messenger.py +1 -1
  4. {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_reader.py +98 -54
  5. {serverish-1.2.0 → serverish-1.2.1}/LICENSE +0 -0
  6. {serverish-1.2.0 → serverish-1.2.1}/README.md +0 -0
  7. {serverish-1.2.0 → serverish-1.2.1}/serverish/__init__.py +0 -0
  8. {serverish-1.2.0 → serverish-1.2.1}/serverish/base/__init__.py +0 -0
  9. {serverish-1.2.0 → serverish-1.2.1}/serverish/base/asyncio_util_functions.py +0 -0
  10. {serverish-1.2.0 → serverish-1.2.1}/serverish/base/collector.py +0 -0
  11. {serverish-1.2.0 → serverish-1.2.1}/serverish/base/datetime.py +0 -0
  12. {serverish-1.2.0 → serverish-1.2.1}/serverish/base/exceptions.py +0 -0
  13. {serverish-1.2.0 → serverish-1.2.1}/serverish/base/fifoset.py +0 -0
  14. {serverish-1.2.0 → serverish-1.2.1}/serverish/base/hasstatuses.py +0 -0
  15. {serverish-1.2.0 → serverish-1.2.1}/serverish/base/idmanger.py +0 -0
  16. {serverish-1.2.0 → serverish-1.2.1}/serverish/base/manageable.py +0 -0
  17. {serverish-1.2.0 → serverish-1.2.1}/serverish/base/singleton.py +0 -0
  18. {serverish-1.2.0 → serverish-1.2.1}/serverish/base/status.py +0 -0
  19. {serverish-1.2.0 → serverish-1.2.1}/serverish/base/task_manager.py +0 -0
  20. {serverish-1.2.0 → serverish-1.2.1}/serverish/connection/__init__.py +0 -0
  21. {serverish-1.2.0 → serverish-1.2.1}/serverish/connection/connection.py +0 -0
  22. {serverish-1.2.0 → serverish-1.2.1}/serverish/connection/connection_jets.py +0 -0
  23. {serverish-1.2.0 → serverish-1.2.1}/serverish/connection/connection_nats.py +0 -0
  24. {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/__init__.py +0 -0
  25. {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/logging.py +0 -0
  26. {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_callback_sub.py +0 -0
  27. {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_journal_pub.py +0 -0
  28. {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_journal_read.py +0 -0
  29. {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_progress_pub.py +0 -0
  30. {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_progress_read.py +0 -0
  31. {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_publisher.py +0 -0
  32. {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_rpc_req.py +0 -0
  33. {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_rpc_resp.py +0 -0
  34. {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_single_pub.py +0 -0
  35. {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msg_single_read.py +0 -0
  36. {serverish-1.2.0 → serverish-1.2.1}/serverish/messenger/msgvalidator.py +0 -0
  37. {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/config.schema.json +0 -0
  38. {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/default.schema.json +0 -0
  39. {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/include/timestamp.schema.json +0 -0
  40. {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/journal.schema.json +0 -0
  41. {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/meta.schema.json +0 -0
  42. {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/program_current_state.schema.json +0 -0
  43. {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/program_state.schema.json +0 -0
  44. {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/progress.schema.json +0 -0
  45. {serverish-1.2.0 → serverish-1.2.1}/serverish/schema/telemetry.schema.json +0 -0
  46. {serverish-1.2.0 → serverish-1.2.1}/serverish/services/TODO.txt +0 -0
  47. {serverish-1.2.0 → serverish-1.2.1}/serverish/slog.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: serverish
3
- Version: 1.2.0
3
+ Version: 1.2.1
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.2.0"
3
+ version = "1.2.1"
4
4
  description = "helpers for server alike projects"
5
5
  authors = ["Mikołaj Kałuszyński", "MMME team"]
6
6
  readme = "README.md"
@@ -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.debug(self.fmt(f"Pulling {self.reader.batch} messages with timeout {timeout}s"))
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.debug(self.fmt(f"Pulled {len(new_msgs)} messages"))
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 pull_subscription._sub.next_msg(timeout=deadline)
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
- # TODO: Check if it is proper format for NATS!
431
- cfg['opt_start_time'] = self.opt_start_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
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