dbos 0.16.0a2__tar.gz → 0.17.0a2__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.

Potentially problematic release.


This version of dbos might be problematic. Click here for more details.

Files changed (79) hide show
  1. {dbos-0.16.0a2 → dbos-0.17.0a2}/PKG-INFO +1 -1
  2. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_core.py +2 -4
  3. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_dbos.py +3 -2
  4. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_outcome.py +17 -7
  5. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_queue.py +2 -2
  6. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_sys_db.py +6 -3
  7. {dbos-0.16.0a2 → dbos-0.17.0a2}/pyproject.toml +1 -1
  8. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_dbos.py +58 -0
  9. {dbos-0.16.0a2 → dbos-0.17.0a2}/LICENSE +0 -0
  10. {dbos-0.16.0a2 → dbos-0.17.0a2}/README.md +0 -0
  11. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/__init__.py +0 -0
  12. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_admin_server.py +0 -0
  13. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_app_db.py +0 -0
  14. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_classproperty.py +0 -0
  15. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_context.py +0 -0
  16. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_croniter.py +0 -0
  17. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_dbos_config.py +0 -0
  18. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_error.py +0 -0
  19. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_fastapi.py +0 -0
  20. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_flask.py +0 -0
  21. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_kafka.py +0 -0
  22. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_kafka_message.py +0 -0
  23. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_logger.py +0 -0
  24. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_migrations/env.py +0 -0
  25. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_migrations/script.py.mako +0 -0
  26. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  27. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  28. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  29. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  30. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  31. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  32. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_recovery.py +0 -0
  33. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_registrations.py +0 -0
  34. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_request.py +0 -0
  35. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_roles.py +0 -0
  36. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_scheduler.py +0 -0
  37. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_schemas/__init__.py +0 -0
  38. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_schemas/application_database.py +0 -0
  39. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_schemas/system_database.py +0 -0
  40. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_serialization.py +0 -0
  41. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_templates/hello/README.md +0 -0
  42. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_templates/hello/__package/__init__.py +0 -0
  43. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_templates/hello/__package/main.py +0 -0
  44. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_templates/hello/__package/schema.py +0 -0
  45. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_templates/hello/alembic.ini +0 -0
  46. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_templates/hello/dbos-config.yaml.dbos +0 -0
  47. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_templates/hello/migrations/env.py.dbos +0 -0
  48. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_templates/hello/migrations/script.py.mako +0 -0
  49. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_templates/hello/migrations/versions/2024_07_31_180642_init.py +0 -0
  50. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_templates/hello/start_postgres_docker.py +0 -0
  51. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/_tracer.py +0 -0
  52. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/cli.py +0 -0
  53. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/dbos-config.schema.json +0 -0
  54. {dbos-0.16.0a2 → dbos-0.17.0a2}/dbos/py.typed +0 -0
  55. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/__init__.py +0 -0
  56. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/atexit_no_ctor.py +0 -0
  57. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/atexit_no_launch.py +0 -0
  58. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/classdefs.py +0 -0
  59. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/conftest.py +0 -0
  60. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/more_classdefs.py +0 -0
  61. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_admin_server.py +0 -0
  62. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_async.py +0 -0
  63. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_classdecorators.py +0 -0
  64. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_concurrency.py +0 -0
  65. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_config.py +0 -0
  66. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_croniter.py +0 -0
  67. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_failures.py +0 -0
  68. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_fastapi.py +0 -0
  69. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_fastapi_roles.py +0 -0
  70. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_flask.py +0 -0
  71. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_kafka.py +0 -0
  72. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_outcome.py +0 -0
  73. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_package.py +0 -0
  74. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_queue.py +0 -0
  75. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_scheduler.py +0 -0
  76. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_schema_migration.py +0 -0
  77. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_singleton.py +0 -0
  78. {dbos-0.16.0a2 → dbos-0.17.0a2}/tests/test_spans.py +0 -0
  79. {dbos-0.16.0a2 → dbos-0.17.0a2}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.16.0a2
3
+ Version: 0.17.0a2
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -719,7 +719,7 @@ def decorate_step(
719
719
  finally:
720
720
  dbos._sys_db.record_operation_result(step_output)
721
721
 
722
- def check_existing_result() -> Optional[R]:
722
+ def check_existing_result() -> Optional[str]:
723
723
  ctx = assert_current_dbos_context()
724
724
  recorded_output = dbos._sys_db.check_operation_execution(
725
725
  ctx.workflow_id, ctx.function_id
@@ -734,9 +734,7 @@ def decorate_step(
734
734
  )
735
735
  raise deserialized_error
736
736
  elif recorded_output["output"] is not None:
737
- return cast(
738
- R, _serialization.deserialize(recorded_output["output"])
739
- )
737
+ return recorded_output["output"]
740
738
  else:
741
739
  raise Exception("Output and error are both None")
742
740
  else:
@@ -45,7 +45,7 @@ from ._core import (
45
45
  start_workflow,
46
46
  workflow_wrapper,
47
47
  )
48
- from ._queue import Queue, _queue_thread
48
+ from ._queue import Queue, queue_thread
49
49
  from ._recovery import recover_pending_workflows, startup_recovery_thread
50
50
  from ._registrations import (
51
51
  DEFAULT_MAX_RECOVERY_ATTEMPTS,
@@ -283,6 +283,7 @@ class DBOS:
283
283
  self.flask: Optional["Flask"] = flask
284
284
  self._executor_field: Optional[ThreadPoolExecutor] = None
285
285
  self._background_threads: List[threading.Thread] = []
286
+ self._executor_id: str = os.environ.get("DBOS__VMID", "local")
286
287
 
287
288
  # If using FastAPI, set up middleware and lifecycle events
288
289
  if self.fastapi is not None:
@@ -383,7 +384,7 @@ class DBOS:
383
384
  evt = threading.Event()
384
385
  self.stop_events.append(evt)
385
386
  bg_queue_thread = threading.Thread(
386
- target=_queue_thread, args=(evt, self), daemon=True
387
+ target=queue_thread, args=(evt, self), daemon=True
387
388
  )
388
389
  bg_queue_thread.start()
389
390
  self._background_threads.append(bg_queue_thread)
@@ -4,6 +4,8 @@ import inspect
4
4
  import time
5
5
  from typing import Any, Callable, Coroutine, Optional, Protocol, TypeVar, Union, cast
6
6
 
7
+ from . import _serialization
8
+
7
9
  T = TypeVar("T")
8
10
  R = TypeVar("R")
9
11
 
@@ -28,7 +30,7 @@ class Outcome(Protocol[T]):
28
30
  exceeded_retries: Callable[[int], BaseException],
29
31
  ) -> "Outcome[T]": ...
30
32
 
31
- def intercept(self, interceptor: Callable[[], Optional[T]]) -> "Outcome[T]": ...
33
+ def intercept(self, interceptor: Callable[[], Optional[str]]) -> "Outcome[T]": ...
32
34
 
33
35
  def __call__(self) -> Union[T, Coroutine[Any, Any, T]]: ...
34
36
 
@@ -58,11 +60,15 @@ class Immediate(Outcome[T]):
58
60
  return Immediate(lambda: before()(self._func))
59
61
 
60
62
  @staticmethod
61
- def _intercept(func: Callable[[], T], interceptor: Callable[[], Optional[T]]) -> T:
63
+ def _intercept(
64
+ func: Callable[[], T], interceptor: Callable[[], Optional[str]]
65
+ ) -> T:
62
66
  intercepted = interceptor()
63
- return intercepted if intercepted else func()
67
+ return (
68
+ cast(T, _serialization.deserialize(intercepted)) if intercepted else func()
69
+ )
64
70
 
65
- def intercept(self, interceptor: Callable[[], Optional[T]]) -> "Immediate[T]":
71
+ def intercept(self, interceptor: Callable[[], Optional[str]]) -> "Immediate[T]":
66
72
  return Immediate[T](lambda: Immediate._intercept(self._func, interceptor))
67
73
 
68
74
  @staticmethod
@@ -151,12 +157,16 @@ class Pending(Outcome[T]):
151
157
  @staticmethod
152
158
  async def _intercept(
153
159
  func: Callable[[], Coroutine[Any, Any, T]],
154
- interceptor: Callable[[], Optional[T]],
160
+ interceptor: Callable[[], Optional[str]],
155
161
  ) -> T:
156
162
  intercepted = await asyncio.to_thread(interceptor)
157
- return intercepted if intercepted else await func()
163
+ return (
164
+ cast(T, _serialization.deserialize(intercepted))
165
+ if intercepted
166
+ else await func()
167
+ )
158
168
 
159
- def intercept(self, interceptor: Callable[[], Optional[T]]) -> "Pending[T]":
169
+ def intercept(self, interceptor: Callable[[], Optional[str]]) -> "Pending[T]":
160
170
  return Pending[T](lambda: Pending._intercept(self._func, interceptor))
161
171
 
162
172
  @staticmethod
@@ -51,13 +51,13 @@ class Queue:
51
51
  return start_workflow(dbos, func, self.name, False, *args, **kwargs)
52
52
 
53
53
 
54
- def _queue_thread(stop_event: threading.Event, dbos: "DBOS") -> None:
54
+ def queue_thread(stop_event: threading.Event, dbos: "DBOS") -> None:
55
55
  while not stop_event.is_set():
56
56
  if stop_event.wait(timeout=1):
57
57
  return
58
58
  for _, queue in dbos._registry.queue_info_map.items():
59
59
  try:
60
- wf_ids = dbos._sys_db.start_queued_workflows(queue)
60
+ wf_ids = dbos._sys_db.start_queued_workflows(queue, dbos._executor_id)
61
61
  for id in wf_ids:
62
62
  execute_workflow_by_id(dbos, id)
63
63
  except Exception:
@@ -1104,7 +1104,7 @@ class SystemDatabase:
1104
1104
  .on_conflict_do_nothing()
1105
1105
  )
1106
1106
 
1107
- def start_queued_workflows(self, queue: "Queue") -> List[str]:
1107
+ def start_queued_workflows(self, queue: "Queue", executor_id: str) -> List[str]:
1108
1108
  start_time_ms = int(time.time() * 1000)
1109
1109
  if queue.limiter is not None:
1110
1110
  limiter_period_ms = int(queue.limiter["period"] * 1000)
@@ -1159,7 +1159,7 @@ class SystemDatabase:
1159
1159
  if len(ret_ids) + num_recent_queries >= queue.limiter["limit"]:
1160
1160
  break
1161
1161
 
1162
- # To start a function, first set its status to PENDING
1162
+ # To start a function, first set its status to PENDING and update its executor ID
1163
1163
  c.execute(
1164
1164
  SystemSchema.workflow_status.update()
1165
1165
  .where(SystemSchema.workflow_status.c.workflow_uuid == id)
@@ -1167,7 +1167,10 @@ class SystemDatabase:
1167
1167
  SystemSchema.workflow_status.c.status
1168
1168
  == WorkflowStatusString.ENQUEUED.value
1169
1169
  )
1170
- .values(status=WorkflowStatusString.PENDING.value)
1170
+ .values(
1171
+ status=WorkflowStatusString.PENDING.value,
1172
+ executor_id=executor_id,
1173
+ )
1171
1174
  )
1172
1175
 
1173
1176
  # Then give it a start time
@@ -23,7 +23,7 @@ dependencies = [
23
23
  ]
24
24
  requires-python = ">=3.9"
25
25
  readme = "README.md"
26
- version = "0.16.0a2"
26
+ version = "0.17.0a2"
27
27
 
28
28
  [project.license]
29
29
  text = "MIT"
@@ -374,6 +374,64 @@ def test_recovery_workflow(dbos: DBOS) -> None:
374
374
  assert stat.recovery_attempts == 1
375
375
 
376
376
 
377
+ def test_recovery_workflow_step(dbos: DBOS) -> None:
378
+ step_counter: int = 0
379
+ wf_counter: int = 0
380
+
381
+ @DBOS.workflow()
382
+ def test_workflow(var: str, var2: str) -> str:
383
+ nonlocal wf_counter
384
+ wf_counter += 1
385
+ test_step(var2)
386
+ return var
387
+
388
+ @DBOS.step()
389
+ def test_step(var2: str) -> None:
390
+ nonlocal step_counter
391
+ step_counter += 1
392
+ print(f"I'm a test_step {var2}!")
393
+ return
394
+
395
+ wfuuid = str(uuid.uuid4())
396
+ with SetWorkflowID(wfuuid):
397
+ assert test_workflow("bob", "bob") == "bob"
398
+
399
+ dbos._sys_db.wait_for_buffer_flush()
400
+ # Change the workflow status to pending
401
+ dbos._sys_db.update_workflow_status(
402
+ {
403
+ "workflow_uuid": wfuuid,
404
+ "status": "PENDING",
405
+ "name": test_workflow.__qualname__,
406
+ "class_name": None,
407
+ "config_name": None,
408
+ "output": None,
409
+ "error": None,
410
+ "executor_id": None,
411
+ "app_id": None,
412
+ "app_version": None,
413
+ "request": None,
414
+ "recovery_attempts": None,
415
+ "authenticated_user": None,
416
+ "authenticated_roles": None,
417
+ "assumed_role": None,
418
+ "queue_name": None,
419
+ }
420
+ )
421
+
422
+ # Recovery should execute the workflow again but skip the transaction
423
+ workflow_handles = DBOS.recover_pending_workflows()
424
+ assert len(workflow_handles) == 1
425
+ assert workflow_handles[0].get_result() == "bob"
426
+ assert wf_counter == 2
427
+ assert step_counter == 1
428
+
429
+ # Test that there was a recovery attempt of this
430
+ stat = workflow_handles[0].get_status()
431
+ assert stat
432
+ assert stat.recovery_attempts == 1
433
+
434
+
377
435
  def test_recovery_temp_workflow(dbos: DBOS) -> None:
378
436
  txn_counter: int = 0
379
437
 
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