dbos 2.0.0__py3-none-any.whl → 2.1.0__py3-none-any.whl

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.
dbos/_core.py CHANGED
@@ -1278,21 +1278,24 @@ def recv(dbos: "DBOS", topic: Optional[str] = None, timeout_seconds: float = 60)
1278
1278
  def set_event(dbos: "DBOS", key: str, value: Any) -> None:
1279
1279
  cur_ctx = get_local_dbos_context()
1280
1280
  if cur_ctx is not None:
1281
- # Must call it within a workflow
1282
- assert (
1283
- cur_ctx.is_workflow()
1284
- ), "set_event() must be called from within a workflow"
1285
- attributes: TracedAttributes = {
1286
- "name": "set_event",
1287
- }
1288
- with EnterDBOSStep(attributes):
1289
- ctx = assert_current_dbos_context()
1290
- dbos._sys_db.set_event(
1291
- ctx.workflow_id, ctx.curr_step_function_id, key, value
1281
+ if cur_ctx.is_workflow():
1282
+ # If called from a workflow function, run as a step
1283
+ attributes: TracedAttributes = {
1284
+ "name": "set_event",
1285
+ }
1286
+ with EnterDBOSStep(attributes):
1287
+ ctx = assert_current_dbos_context()
1288
+ dbos._sys_db.set_event_from_workflow(
1289
+ ctx.workflow_id, ctx.curr_step_function_id, key, value
1290
+ )
1291
+ elif cur_ctx.is_step():
1292
+ dbos._sys_db.set_event_from_step(cur_ctx.workflow_id, key, value)
1293
+ else:
1294
+ raise DBOSException(
1295
+ "set_event() must be called from within a workflow or step"
1292
1296
  )
1293
1297
  else:
1294
- # Cannot call it from outside of a workflow
1295
- raise DBOSException("set_event() must be called from within a workflow")
1298
+ raise DBOSException("set_event() must be called from within a workflow or step")
1296
1299
 
1297
1300
 
1298
1301
  def get_event(
dbos/_debouncer.py CHANGED
@@ -86,6 +86,7 @@ def debouncer_workflow(
86
86
  dbos = _get_dbos_instance()
87
87
 
88
88
  workflow_inputs: WorkflowInputs = {"args": args, "kwargs": kwargs}
89
+
89
90
  # Every time the debounced workflow is called, a message is sent to this workflow.
90
91
  # It waits until debounce_period_sec have passed since the last message or until
91
92
  # debounce_timeout_sec has elapsed.
@@ -95,7 +96,10 @@ def debouncer_workflow(
95
96
  if options["debounce_timeout_sec"]
96
97
  else math.inf
97
98
  )
98
- debounce_deadline_epoch_sec = dbos._sys_db.call_function_as_step(get_debounce_deadline_epoch_sec, "get_debounce_deadline_epoch_sec")
99
+
100
+ debounce_deadline_epoch_sec = dbos._sys_db.call_function_as_step(
101
+ get_debounce_deadline_epoch_sec, "get_debounce_deadline_epoch_sec"
102
+ )
99
103
  debounce_period_sec = initial_debounce_period_sec
100
104
  while time.time() < debounce_deadline_epoch_sec:
101
105
  time_until_deadline = max(debounce_deadline_epoch_sec - time.time(), 0)
dbos/_scheduler.py CHANGED
@@ -1,3 +1,4 @@
1
+ import random
1
2
  import threading
2
3
  import traceback
3
4
  from datetime import datetime, timezone
@@ -15,28 +16,40 @@ from ._registrations import get_dbos_func_name
15
16
 
16
17
  ScheduledWorkflow = Callable[[datetime, datetime], None]
17
18
 
18
- scheduler_queue: Queue
19
-
20
19
 
21
20
  def scheduler_loop(
22
21
  func: ScheduledWorkflow, cron: str, stop_event: threading.Event
23
22
  ) -> None:
23
+ from dbos._dbos import _get_dbos_instance
24
+
25
+ dbos = _get_dbos_instance()
26
+ scheduler_queue = dbos._registry.get_internal_queue()
24
27
  try:
25
28
  iter = croniter(cron, datetime.now(timezone.utc), second_at_beginning=True)
26
- except Exception as e:
29
+ except Exception:
27
30
  dbos_logger.error(
28
31
  f'Cannot run scheduled function {get_dbos_func_name(func)}. Invalid crontab "{cron}"'
29
32
  )
33
+ raise
30
34
  while not stop_event.is_set():
31
- nextExecTime = iter.get_next(datetime)
32
- sleepTime = nextExecTime - datetime.now(timezone.utc)
33
- if stop_event.wait(timeout=sleepTime.total_seconds()):
35
+ next_exec_time = iter.get_next(datetime)
36
+ sleep_time = (next_exec_time - datetime.now(timezone.utc)).total_seconds()
37
+ sleep_time = max(0, sleep_time)
38
+ # To prevent a "thundering herd" problem in a distributed setting,
39
+ # apply jitter of up to 10% the sleep time, capped at 10 seconds
40
+ max_jitter = min(sleep_time / 10, 10)
41
+ jitter = random.uniform(0, max_jitter)
42
+ if stop_event.wait(timeout=sleep_time + jitter):
34
43
  return
35
44
  try:
36
- with SetWorkflowID(
37
- f"sched-{get_dbos_func_name(func)}-{nextExecTime.isoformat()}"
38
- ):
39
- scheduler_queue.enqueue(func, nextExecTime, datetime.now(timezone.utc))
45
+ workflowID = (
46
+ f"sched-{get_dbos_func_name(func)}-{next_exec_time.isoformat()}"
47
+ )
48
+ if not dbos._sys_db.get_workflow_status(workflowID):
49
+ with SetWorkflowID(workflowID):
50
+ scheduler_queue.enqueue(
51
+ func, next_exec_time, datetime.now(timezone.utc)
52
+ )
40
53
  except Exception:
41
54
  dbos_logger.warning(
42
55
  f"Exception encountered in scheduler thread: {traceback.format_exc()})"
@@ -49,13 +62,10 @@ def scheduled(
49
62
  def decorator(func: ScheduledWorkflow) -> ScheduledWorkflow:
50
63
  try:
51
64
  croniter(cron, datetime.now(timezone.utc), second_at_beginning=True)
52
- except Exception as e:
65
+ except Exception:
53
66
  raise ValueError(
54
67
  f'Invalid crontab "{cron}" for scheduled function function {get_dbos_func_name(func)}.'
55
68
  )
56
-
57
- global scheduler_queue
58
- scheduler_queue = dbosreg.get_internal_queue()
59
69
  stop_event = threading.Event()
60
70
  dbosreg.register_poller(stop_event, scheduler_loop, func, cron, stop_event)
61
71
  return func
dbos/_sys_db.py CHANGED
@@ -1077,24 +1077,23 @@ class SystemDatabase(ABC):
1077
1077
  SystemSchema.operation_outputs.c.child_workflow_id,
1078
1078
  ).where(SystemSchema.operation_outputs.c.workflow_uuid == workflow_id)
1079
1079
  ).fetchall()
1080
- return [
1081
- StepInfo(
1080
+ steps = []
1081
+ for row in rows:
1082
+ _, output, exception = _serialization.safe_deserialize(
1083
+ workflow_id,
1084
+ serialized_input=None,
1085
+ serialized_output=row[2],
1086
+ serialized_exception=row[3],
1087
+ )
1088
+ step = StepInfo(
1082
1089
  function_id=row[0],
1083
1090
  function_name=row[1],
1084
- output=(
1085
- _serialization.deserialize(row[2])
1086
- if row[2] is not None
1087
- else row[2]
1088
- ),
1089
- error=(
1090
- _serialization.deserialize_exception(row[3])
1091
- if row[3] is not None
1092
- else row[3]
1093
- ),
1091
+ output=output,
1092
+ error=exception,
1094
1093
  child_workflow_id=row[4],
1095
1094
  )
1096
- for row in rows
1097
- ]
1095
+ steps.append(step)
1096
+ return steps
1098
1097
 
1099
1098
  def _record_operation_result_txn(
1100
1099
  self, result: OperationResultInternal, conn: sa.Connection
@@ -1525,7 +1524,7 @@ class SystemDatabase(ABC):
1525
1524
  return duration
1526
1525
 
1527
1526
  @db_retry()
1528
- def set_event(
1527
+ def set_event_from_workflow(
1529
1528
  self,
1530
1529
  workflow_uuid: str,
1531
1530
  function_id: int,
@@ -1567,6 +1566,26 @@ class SystemDatabase(ABC):
1567
1566
  }
1568
1567
  self._record_operation_result_txn(output, conn=c)
1569
1568
 
1569
+ def set_event_from_step(
1570
+ self,
1571
+ workflow_uuid: str,
1572
+ key: str,
1573
+ message: Any,
1574
+ ) -> None:
1575
+ with self.engine.begin() as c:
1576
+ c.execute(
1577
+ self.dialect.insert(SystemSchema.workflow_events)
1578
+ .values(
1579
+ workflow_uuid=workflow_uuid,
1580
+ key=key,
1581
+ value=_serialization.serialize(message),
1582
+ )
1583
+ .on_conflict_do_update(
1584
+ index_elements=["workflow_uuid", "key"],
1585
+ set_={"value": _serialization.serialize(message)},
1586
+ )
1587
+ )
1588
+
1570
1589
  def get_all_events(self, workflow_id: str) -> Dict[str, Any]:
1571
1590
  """
1572
1591
  Get all events currently present for a workflow ID.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -1,7 +1,7 @@
1
- dbos-2.0.0.dist-info/METADATA,sha256=iI909H18rHrExRmKN1-N3VhzsxkcVem60bg22gBQKFo,14530
2
- dbos-2.0.0.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
- dbos-2.0.0.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
- dbos-2.0.0.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
1
+ dbos-2.1.0.dist-info/METADATA,sha256=CWKQyVwcSDagiayTT1InoAHg9tXwr39qxOOLIQsCY4A,14530
2
+ dbos-2.1.0.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
+ dbos-2.1.0.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
+ dbos-2.1.0.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
5
5
  dbos/__init__.py,sha256=pT4BuNLDCrIQX27vQG8NlfxX6PZRU7r9miq4thJTszU,982
6
6
  dbos/__main__.py,sha256=G7Exn-MhGrVJVDbgNlpzhfh8WMX_72t3_oJaFT9Lmt8,653
7
7
  dbos/_admin_server.py,sha256=hubQJw5T8zGKCPNS6FQTXy8jQ8GTJxoYQaDTMlICl9k,16267
@@ -11,11 +11,11 @@ dbos/_client.py,sha256=ppkO3bJ_qpBzWjYf9BkGfy8yF76e91UuhoLMtbIwXnU,18933
11
11
  dbos/_conductor/conductor.py,sha256=3E_hL3c9g9yWqKZkvI6KA0-ZzPMPRo06TOzT1esMiek,24114
12
12
  dbos/_conductor/protocol.py,sha256=q3rgLxINFtWFigdOONc-4gX4vn66UmMlJQD6Kj8LnL4,7420
13
13
  dbos/_context.py,sha256=cJDxVbswTLXKE5MV4Hmg6gpIX3Dd5mBTG-4lmofWP9E,27668
14
- dbos/_core.py,sha256=13DNN_fpSIs42NquV80XsHV7yKwY_adKP03h_xhXok4,50493
14
+ dbos/_core.py,sha256=x0kGmYD2RAPsM6NCOW9b64FKo6xHaQEail8ld2nDWzc,50685
15
15
  dbos/_croniter.py,sha256=XHAyUyibs_59sJQfSNWkP7rqQY6_XrlfuuCxk4jYqek,47559
16
16
  dbos/_dbos.py,sha256=lkOnSZrEPkqURBJatBhnslHO0OKgmrS8rqLowEn5Rr0,58441
17
17
  dbos/_dbos_config.py,sha256=4vgPyy4NiojAOiw3BnjWwiwT1_Ju3ZhXqJQOKDXhsE4,25148
18
- dbos/_debouncer.py,sha256=9-9dlXKLRHSUSylprCw18r-7MAdFwD8-w0KkY-bEG_I,15281
18
+ dbos/_debouncer.py,sha256=qNjIVmWqTPp64M2cEbLnpgGmlKVdCaAKysD1BPJgWh4,15297
19
19
  dbos/_debug.py,sha256=0MfgNqutCUhI4PEmmra9x7f3DiFE_0nscfUCHdLimEY,1415
20
20
  dbos/_docker_pg_helper.py,sha256=xySum4hTA8TVMBODoG19u4cXQAB1vCock-jwM2pnmSI,7791
21
21
  dbos/_error.py,sha256=GwO0Ng4d4iB52brY09-Ss6Cz_V28Xc0D0cRCzZ6XmNM,8688
@@ -31,12 +31,12 @@ dbos/_queue.py,sha256=cgFFwVPUeQtrTgk7ivoTZb0v9ya8rZK4m7-G-h5gIb4,4846
31
31
  dbos/_recovery.py,sha256=K-wlFhdf4yGRm6cUzyhcTjQUS0xp2T5rdNMLiiBErYg,2882
32
32
  dbos/_registrations.py,sha256=bEOntObnWaBylnebr5ZpcX2hk7OVLDd1z4BvW4_y3zA,7380
33
33
  dbos/_roles.py,sha256=kCuhhg8XLtrHCgKgm44I0abIRTGHltf88OwjEKAUggk,2317
34
- dbos/_scheduler.py,sha256=CWeGVfl9h51VXfxt80y5Da_5pE8SPty_AYkfpJkkMxQ,2117
34
+ dbos/_scheduler.py,sha256=n96dNzKMr6-2RQvMxRI6BaoExHbLjw0Kr46j1P-DjP4,2620
35
35
  dbos/_schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  dbos/_schemas/application_database.py,sha256=SypAS9l9EsaBHFn9FR8jmnqt01M74d9AF1AMa4m2hhI,1040
37
37
  dbos/_schemas/system_database.py,sha256=aEkjRQDh9xjdke0d9uFx_20-c9UjQtvuLtHZ24aOypA,5497
38
38
  dbos/_serialization.py,sha256=GLgWLtHpvk7nSHyXukVQLE1ASNA3CJBtfF8w6iflBDw,3590
39
- dbos/_sys_db.py,sha256=lv7ahoD2E0eIzZCfaaHpDJ40XlJsbwPjEX_Sju3b9ew,84558
39
+ dbos/_sys_db.py,sha256=kvssT1I6-vW6eGKmbDqr4L-HO-2xbwkePO0TwVxRTM8,85203
40
40
  dbos/_sys_db_postgres.py,sha256=GuyGVyZZD_Wl7LjRSkHnOuZ-hOROlO4Xs2UeDhKq10E,6963
41
41
  dbos/_sys_db_sqlite.py,sha256=ifjKdy-Z9vlVIBf5L6XnSaNjiBdvqPE73asVHim4A5Q,6998
42
42
  dbos/_templates/dbos-db-starter/README.md,sha256=GhxhBj42wjTt1fWEtwNriHbJuKb66Vzu89G4pxNHw2g,930
@@ -56,4 +56,4 @@ dbos/cli/migration.py,sha256=zJnDPUBnil5XZXFxc01EbZ0Radw_y8wtDGZExgelAxc,3633
56
56
  dbos/dbos-config.schema.json,sha256=47wofTZ5jlFynec7bG0L369tAXbRQQ2euBxBXvg4m9c,1730
57
57
  dbos/py.typed,sha256=QfzXT1Ktfk3Rj84akygc7_42z0lRpCq0Ilh8OXI6Zas,44
58
58
  version/__init__.py,sha256=L4sNxecRuqdtSFdpUGX3TtBi9KL3k7YsZVIvv-fv9-A,1678
59
- dbos-2.0.0.dist-info/RECORD,,
59
+ dbos-2.1.0.dist-info/RECORD,,
File without changes