dbos 1.15.0a9__tar.gz → 2.1.0__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 (99) hide show
  1. {dbos-1.15.0a9 → dbos-2.1.0}/PKG-INFO +1 -1
  2. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_core.py +16 -13
  3. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_debouncer.py +5 -1
  4. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_scheduler.py +24 -14
  5. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_sys_db.py +34 -15
  6. {dbos-1.15.0a9 → dbos-2.1.0}/pyproject.toml +1 -1
  7. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_dbos.py +31 -15
  8. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_failures.py +31 -0
  9. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_scheduler.py +7 -7
  10. {dbos-1.15.0a9 → dbos-2.1.0}/LICENSE +0 -0
  11. {dbos-1.15.0a9 → dbos-2.1.0}/README.md +0 -0
  12. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/__init__.py +0 -0
  13. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/__main__.py +0 -0
  14. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_admin_server.py +0 -0
  15. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_app_db.py +0 -0
  16. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_classproperty.py +0 -0
  17. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_client.py +0 -0
  18. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_conductor/conductor.py +0 -0
  19. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_conductor/protocol.py +0 -0
  20. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_context.py +0 -0
  21. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_croniter.py +0 -0
  22. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_dbos.py +0 -0
  23. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_dbos_config.py +0 -0
  24. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_debug.py +0 -0
  25. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_docker_pg_helper.py +0 -0
  26. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_error.py +0 -0
  27. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_event_loop.py +0 -0
  28. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_fastapi.py +0 -0
  29. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_flask.py +0 -0
  30. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_kafka.py +0 -0
  31. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_kafka_message.py +0 -0
  32. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_logger.py +0 -0
  33. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_migration.py +0 -0
  34. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_outcome.py +0 -0
  35. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_queue.py +0 -0
  36. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_recovery.py +0 -0
  37. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_registrations.py +0 -0
  38. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_roles.py +0 -0
  39. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_schemas/__init__.py +0 -0
  40. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_schemas/application_database.py +0 -0
  41. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_schemas/system_database.py +0 -0
  42. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_serialization.py +0 -0
  43. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_sys_db_postgres.py +0 -0
  44. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_sys_db_sqlite.py +0 -0
  45. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_templates/dbos-db-starter/README.md +0 -0
  46. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  47. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_templates/dbos-db-starter/__package/main.py.dbos +0 -0
  48. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  49. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  50. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_templates/dbos-db-starter/migrations/create_table.py.dbos +0 -0
  51. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  52. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_tracer.py +0 -0
  53. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_utils.py +0 -0
  54. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/_workflow_commands.py +0 -0
  55. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/cli/_github_init.py +0 -0
  56. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/cli/_template_init.py +0 -0
  57. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/cli/cli.py +0 -0
  58. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/cli/migration.py +0 -0
  59. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/dbos-config.schema.json +0 -0
  60. {dbos-1.15.0a9 → dbos-2.1.0}/dbos/py.typed +0 -0
  61. {dbos-1.15.0a9 → dbos-2.1.0}/tests/__init__.py +0 -0
  62. {dbos-1.15.0a9 → dbos-2.1.0}/tests/atexit_no_ctor.py +0 -0
  63. {dbos-1.15.0a9 → dbos-2.1.0}/tests/atexit_no_launch.py +0 -0
  64. {dbos-1.15.0a9 → dbos-2.1.0}/tests/classdefs.py +0 -0
  65. {dbos-1.15.0a9 → dbos-2.1.0}/tests/client_collateral.py +0 -0
  66. {dbos-1.15.0a9 → dbos-2.1.0}/tests/client_worker.py +0 -0
  67. {dbos-1.15.0a9 → dbos-2.1.0}/tests/conftest.py +0 -0
  68. {dbos-1.15.0a9 → dbos-2.1.0}/tests/dupname_classdefs1.py +0 -0
  69. {dbos-1.15.0a9 → dbos-2.1.0}/tests/dupname_classdefsa.py +0 -0
  70. {dbos-1.15.0a9 → dbos-2.1.0}/tests/more_classdefs.py +0 -0
  71. {dbos-1.15.0a9 → dbos-2.1.0}/tests/queuedworkflow.py +0 -0
  72. {dbos-1.15.0a9 → dbos-2.1.0}/tests/script_without_fastapi.py +0 -0
  73. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_admin_server.py +0 -0
  74. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_async.py +0 -0
  75. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_async_workflow_management.py +0 -0
  76. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_classdecorators.py +0 -0
  77. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_cli.py +0 -0
  78. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_client.py +0 -0
  79. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_concurrency.py +0 -0
  80. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_config.py +0 -0
  81. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_croniter.py +0 -0
  82. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_debouncer.py +0 -0
  83. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_debug.py +0 -0
  84. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_docker_secrets.py +0 -0
  85. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_fastapi.py +0 -0
  86. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_fastapi_roles.py +0 -0
  87. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_flask.py +0 -0
  88. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_kafka.py +0 -0
  89. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_outcome.py +0 -0
  90. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_package.py +0 -0
  91. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_queue.py +0 -0
  92. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_schema_migration.py +0 -0
  93. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_singleton.py +0 -0
  94. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_spans.py +0 -0
  95. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_sqlalchemy.py +0 -0
  96. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_streaming.py +0 -0
  97. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_workflow_introspection.py +0 -0
  98. {dbos-1.15.0a9 → dbos-2.1.0}/tests/test_workflow_management.py +0 -0
  99. {dbos-1.15.0a9 → dbos-2.1.0}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 1.15.0a9
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
@@ -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(
@@ -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)
@@ -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
@@ -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.
@@ -34,7 +34,7 @@ classifiers = [
34
34
  "Topic :: Software Development :: Libraries :: Python Modules",
35
35
  "Framework :: AsyncIO",
36
36
  ]
37
- version = "1.15.0a9"
37
+ version = "2.1.0"
38
38
 
39
39
  [project.license]
40
40
  text = "MIT"
@@ -965,41 +965,57 @@ def test_send_recv_temp_wf(dbos: DBOS) -> None:
965
965
  def test_set_get_events(dbos: DBOS) -> None:
966
966
  @DBOS.workflow()
967
967
  def test_setevent_workflow() -> None:
968
- dbos.set_event("key1", "value1")
969
- dbos.set_event("key2", "value2")
970
- dbos.set_event("key3", None)
968
+ DBOS.set_event("key1", "value1")
969
+ DBOS.set_event("key2", "value2")
970
+ DBOS.set_event("key3", None)
971
+ set_event_step()
972
+
973
+ @DBOS.step()
974
+ def set_event_step() -> None:
975
+ DBOS.set_event("key4", "value4")
971
976
 
972
977
  @DBOS.workflow()
973
978
  def test_getevent_workflow(
974
- target_uuid: str, key: str, timeout_seconds: float = 10
979
+ target_uuid: str, key: str, timeout: float = 0.0
975
980
  ) -> Optional[str]:
976
- msg = dbos.get_event(target_uuid, key, timeout_seconds)
981
+ msg = dbos.get_event(target_uuid, key, timeout)
977
982
  return str(msg) if msg is not None else None
978
983
 
979
- wfuuid = str(uuid.uuid4())
980
- with SetWorkflowID(wfuuid):
984
+ wfid = str(uuid.uuid4())
985
+ with SetWorkflowID(wfid):
981
986
  test_setevent_workflow()
982
- with SetWorkflowID(wfuuid):
987
+ with SetWorkflowID(wfid):
983
988
  test_setevent_workflow()
984
989
 
985
- value1 = test_getevent_workflow(wfuuid, "key1")
990
+ value1 = test_getevent_workflow(wfid, "key1")
986
991
  assert value1 == "value1"
987
992
 
988
- value2 = test_getevent_workflow(wfuuid, "key2")
993
+ value2 = test_getevent_workflow(wfid, "key2")
989
994
  assert value2 == "value2"
990
995
 
991
996
  # Run getEvent outside of a workflow
992
- value1 = dbos.get_event(wfuuid, "key1")
997
+ value1 = DBOS.get_event(wfid, "key1", 0)
993
998
  assert value1 == "value1"
994
999
 
995
- value2 = dbos.get_event(wfuuid, "key2")
1000
+ value2 = DBOS.get_event(wfid, "key2", 0)
996
1001
  assert value2 == "value2"
997
1002
 
998
1003
  begin_time = time.time()
999
- value3 = test_getevent_workflow(wfuuid, "key3")
1004
+ value3 = test_getevent_workflow(wfid, "key3")
1000
1005
  assert value3 is None
1001
- duration = time.time() - begin_time
1002
- assert duration < 1 # None is from the event not from the timeout
1006
+
1007
+ value4 = DBOS.get_event(wfid, "key4", 0)
1008
+ assert value4 == "value4"
1009
+
1010
+ steps = DBOS.list_workflow_steps(wfid)
1011
+ assert len(steps) == 4
1012
+ assert (
1013
+ steps[0]["function_name"]
1014
+ == steps[1]["function_name"]
1015
+ == steps[2]["function_name"]
1016
+ == "DBOS.setEvent"
1017
+ )
1018
+ assert steps[3]["function_name"] == set_event_step.__qualname__
1003
1019
 
1004
1020
  # Test OAOO
1005
1021
  timeout_uuid = str(uuid.uuid4())
@@ -9,6 +9,7 @@ from psycopg.errors import SerializationFailure
9
9
  from sqlalchemy.exc import InvalidRequestError, OperationalError
10
10
 
11
11
  from dbos import DBOS, Queue, SetWorkflowID
12
+ from dbos._client import DBOSClient
12
13
  from dbos._dbos_config import DBOSConfig
13
14
  from dbos._error import (
14
15
  DBOSAwaitedWorkflowCancelledError,
@@ -502,6 +503,36 @@ def test_error_serialization() -> None:
502
503
  assert isinstance(exception, str)
503
504
 
504
505
 
506
+ def test_workflow_error_serialization(dbos: DBOS, client: DBOSClient) -> None:
507
+
508
+ @DBOS.step()
509
+ def step() -> None:
510
+ raise BadException(1, 2)
511
+
512
+ @DBOS.workflow()
513
+ def workflow() -> None:
514
+ step()
515
+
516
+ handle = DBOS.start_workflow(workflow)
517
+
518
+ with pytest.raises(BadException):
519
+ handle.get_result()
520
+
521
+ workflows = DBOS.list_workflows()
522
+ assert len(workflows) == 1
523
+ assert workflows[0].error is not None
524
+
525
+ steps = DBOS.list_workflow_steps(handle.workflow_id)
526
+ assert len(steps) == 1
527
+ assert steps[0]["error"] is not None
528
+
529
+ status = handle.get_status()
530
+ assert status.error is not None
531
+
532
+ status = client.retrieve_workflow(handle.workflow_id).get_status()
533
+ assert status.error is not None
534
+
535
+
505
536
  def test_unregistered_workflow(dbos: DBOS, config: DBOSConfig) -> None:
506
537
 
507
538
  @DBOS.workflow()
@@ -102,7 +102,7 @@ def test_scheduled_workflow(dbos: DBOS) -> None:
102
102
  wf_counter += 1
103
103
 
104
104
  time.sleep(5)
105
- assert wf_counter > 2 and wf_counter <= 5
105
+ assert wf_counter > 1 and wf_counter <= 5
106
106
 
107
107
 
108
108
  def test_appdb_downtime(dbos: DBOS, skip_with_sqlite: None) -> None:
@@ -123,7 +123,7 @@ def test_appdb_downtime(dbos: DBOS, skip_with_sqlite: None) -> None:
123
123
  time.sleep(2)
124
124
  assert dbos._app_db
125
125
  simulate_db_restart(dbos._app_db.engine, 2)
126
- time.sleep(2)
126
+ time.sleep(3)
127
127
  assert wf_counter > 2
128
128
 
129
129
 
@@ -138,7 +138,7 @@ def test_sysdb_downtime(dbos: DBOS, skip_with_sqlite: None) -> None:
138
138
 
139
139
  time.sleep(2)
140
140
  simulate_db_restart(dbos._sys_db.engine, 2)
141
- time.sleep(2)
141
+ time.sleep(3)
142
142
  # We know there should be at least 2 occurrences from the 4 seconds when the DB was up.
143
143
  # There could be more than 4, depending on the pace the machine...
144
144
  assert wf_counter >= 2
@@ -154,7 +154,7 @@ def test_scheduled_transaction(dbos: DBOS) -> None:
154
154
  txn_counter += 1
155
155
 
156
156
  time.sleep(5)
157
- assert txn_counter > 2 and txn_counter <= 5
157
+ assert txn_counter > 1 and txn_counter <= 5
158
158
 
159
159
 
160
160
  def test_scheduled_step(dbos: DBOS) -> None:
@@ -205,8 +205,8 @@ def test_scheduler_oaoo(dbos: DBOS) -> None:
205
205
  nonlocal txn_counter
206
206
  txn_counter += 1
207
207
 
208
- time.sleep(3)
209
- assert wf_counter >= 1 and wf_counter <= 3
208
+ time.sleep(4)
209
+ assert wf_counter >= 1 and wf_counter <= 4
210
210
  max_tries = 10
211
211
  for i in range(max_tries):
212
212
  try:
@@ -223,7 +223,7 @@ def test_scheduler_oaoo(dbos: DBOS) -> None:
223
223
  evt.set()
224
224
 
225
225
  # Wait for workflows to finish
226
- time.sleep(2)
226
+ time.sleep(3)
227
227
 
228
228
  dbos._sys_db.update_workflow_outcome(workflow_id, "PENDING")
229
229
 
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
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