dbos 0.26.0a13__tar.gz → 0.26.0a14__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 (103) hide show
  1. {dbos-0.26.0a13 → dbos-0.26.0a14}/PKG-INFO +1 -1
  2. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_core.py +3 -1
  3. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_dbos.py +4 -2
  4. dbos-0.26.0a14/dbos/_event_loop.py +67 -0
  5. {dbos-0.26.0a13 → dbos-0.26.0a14}/pyproject.toml +1 -1
  6. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_async.py +70 -4
  7. {dbos-0.26.0a13 → dbos-0.26.0a14}/LICENSE +0 -0
  8. {dbos-0.26.0a13 → dbos-0.26.0a14}/README.md +0 -0
  9. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/__init__.py +0 -0
  10. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/__main__.py +0 -0
  11. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_admin_server.py +0 -0
  12. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_app_db.py +0 -0
  13. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_classproperty.py +0 -0
  14. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_client.py +0 -0
  15. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_conductor/conductor.py +0 -0
  16. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_conductor/protocol.py +0 -0
  17. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_context.py +0 -0
  18. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_croniter.py +0 -0
  19. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_dbos_config.py +0 -0
  20. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_debug.py +0 -0
  21. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_docker_pg_helper.py +0 -0
  22. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_error.py +0 -0
  23. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_fastapi.py +0 -0
  24. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_flask.py +0 -0
  25. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_kafka.py +0 -0
  26. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_kafka_message.py +0 -0
  27. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_logger.py +0 -0
  28. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_migrations/env.py +0 -0
  29. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_migrations/script.py.mako +0 -0
  30. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  31. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  32. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  33. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  34. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  35. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  36. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  37. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  38. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_outcome.py +0 -0
  39. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_queue.py +0 -0
  40. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_recovery.py +0 -0
  41. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_registrations.py +0 -0
  42. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_request.py +0 -0
  43. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_roles.py +0 -0
  44. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_scheduler.py +0 -0
  45. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_schemas/__init__.py +0 -0
  46. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_schemas/application_database.py +0 -0
  47. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_schemas/system_database.py +0 -0
  48. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_serialization.py +0 -0
  49. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_sys_db.py +0 -0
  50. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/README.md +0 -0
  51. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  52. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
  53. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  54. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  55. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  56. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  57. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  58. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  59. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  60. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_tracer.py +0 -0
  61. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_utils.py +0 -0
  62. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/_workflow_commands.py +0 -0
  63. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/cli/_github_init.py +0 -0
  64. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/cli/_template_init.py +0 -0
  65. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/cli/cli.py +0 -0
  66. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/dbos-config.schema.json +0 -0
  67. {dbos-0.26.0a13 → dbos-0.26.0a14}/dbos/py.typed +0 -0
  68. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/__init__.py +0 -0
  69. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/atexit_no_ctor.py +0 -0
  70. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/atexit_no_launch.py +0 -0
  71. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/classdefs.py +0 -0
  72. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/client_collateral.py +0 -0
  73. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/client_worker.py +0 -0
  74. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/conftest.py +0 -0
  75. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/dupname_classdefs1.py +0 -0
  76. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/dupname_classdefsa.py +0 -0
  77. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/more_classdefs.py +0 -0
  78. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/queuedworkflow.py +0 -0
  79. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_admin_server.py +0 -0
  80. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_classdecorators.py +0 -0
  81. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_client.py +0 -0
  82. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_concurrency.py +0 -0
  83. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_config.py +0 -0
  84. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_croniter.py +0 -0
  85. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_dbos.py +0 -0
  86. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_debug.py +0 -0
  87. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_docker_secrets.py +0 -0
  88. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_failures.py +0 -0
  89. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_fastapi.py +0 -0
  90. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_fastapi_roles.py +0 -0
  91. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_flask.py +0 -0
  92. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_kafka.py +0 -0
  93. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_outcome.py +0 -0
  94. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_package.py +0 -0
  95. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_queue.py +0 -0
  96. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_scheduler.py +0 -0
  97. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_schema_migration.py +0 -0
  98. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_singleton.py +0 -0
  99. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_spans.py +0 -0
  100. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_sqlalchemy.py +0 -0
  101. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_workflow_introspection.py +0 -0
  102. {dbos-0.26.0a13 → dbos-0.26.0a14}/tests/test_workflow_management.py +0 -0
  103. {dbos-0.26.0a13 → dbos-0.26.0a14}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.26.0a13
3
+ Version: 0.26.0a14
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -365,7 +365,9 @@ def _execute_workflow_wthread(
365
365
  if isinstance(result, Immediate):
366
366
  return cast(Immediate[R], result)()
367
367
  else:
368
- return asyncio.run(cast(Pending[R], result)())
368
+ return dbos._background_event_loop.submit_coroutine(
369
+ cast(Pending[R], result)()
370
+ )
369
371
  except Exception:
370
372
  dbos.logger.error(
371
373
  f"Exception encountered in asynchronous workflow: {traceback.format_exc()}"
@@ -4,7 +4,6 @@ import asyncio
4
4
  import atexit
5
5
  import hashlib
6
6
  import inspect
7
- import json
8
7
  import os
9
8
  import sys
10
9
  import threading
@@ -31,7 +30,6 @@ from typing import (
31
30
 
32
31
  from opentelemetry.trace import Span
33
32
 
34
- from dbos import _serialization
35
33
  from dbos._conductor.conductor import ConductorWebsocket
36
34
  from dbos._utils import INTERNAL_QUEUE_NAME, GlobalParams
37
35
  from dbos._workflow_commands import (
@@ -112,6 +110,7 @@ from ._error import (
112
110
  DBOSException,
113
111
  DBOSNonExistentWorkflowError,
114
112
  )
113
+ from ._event_loop import BackgroundEventLoop
115
114
  from ._logger import add_otlp_to_all_loggers, config_logger, dbos_logger, init_logger
116
115
  from ._sys_db import SystemDatabase
117
116
  from ._workflow_commands import WorkflowStatus, get_workflow
@@ -341,6 +340,7 @@ class DBOS:
341
340
  self.conductor_url: Optional[str] = conductor_url
342
341
  self.conductor_key: Optional[str] = conductor_key
343
342
  self.conductor_websocket: Optional[ConductorWebsocket] = None
343
+ self._background_event_loop: BackgroundEventLoop = BackgroundEventLoop()
344
344
 
345
345
  init_logger()
346
346
 
@@ -451,6 +451,7 @@ class DBOS:
451
451
  dbos_logger.info(f"Executor ID: {GlobalParams.executor_id}")
452
452
  dbos_logger.info(f"Application version: {GlobalParams.app_version}")
453
453
  self._executor_field = ThreadPoolExecutor(max_workers=64)
454
+ self._background_event_loop.start()
454
455
  self._sys_db_field = SystemDatabase(
455
456
  self.config["database"], debug_mode=debug_mode
456
457
  )
@@ -568,6 +569,7 @@ class DBOS:
568
569
  self._initialized = False
569
570
  for event in self.stop_events:
570
571
  event.set()
572
+ self._background_event_loop.stop()
571
573
  if self._sys_db_field is not None:
572
574
  self._sys_db_field.destroy()
573
575
  self._sys_db_field = None
@@ -0,0 +1,67 @@
1
+ import asyncio
2
+ import threading
3
+ from typing import Any, Coroutine, Optional, TypeVar
4
+
5
+
6
+ class BackgroundEventLoop:
7
+ """
8
+ This is the event loop to which DBOS submits any coroutines that are not started from within an event loop.
9
+ In particular, coroutines submitted to queues (such as from scheduled workflows) run on this event loop.
10
+ """
11
+
12
+ def __init__(self) -> None:
13
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
14
+ self._thread: Optional[threading.Thread] = None
15
+ self._running = False
16
+ self._ready = threading.Event()
17
+
18
+ def start(self) -> None:
19
+ if self._running:
20
+ return
21
+
22
+ self._thread = threading.Thread(target=self._run_event_loop, daemon=True)
23
+ self._thread.start()
24
+ self._ready.wait() # Wait until the loop is running
25
+
26
+ def stop(self) -> None:
27
+ if not self._running or self._loop is None or self._thread is None:
28
+ return
29
+
30
+ asyncio.run_coroutine_threadsafe(self._shutdown(), self._loop)
31
+ self._thread.join()
32
+ self._running = False
33
+
34
+ def _run_event_loop(self) -> None:
35
+ self._loop = asyncio.new_event_loop()
36
+ asyncio.set_event_loop(self._loop)
37
+
38
+ self._running = True
39
+ self._ready.set() # Signal that the loop is ready
40
+
41
+ try:
42
+ self._loop.run_forever()
43
+ finally:
44
+ self._loop.close()
45
+
46
+ async def _shutdown(self) -> None:
47
+ if self._loop is None:
48
+ raise RuntimeError("Event loop not started")
49
+ tasks = [
50
+ task
51
+ for task in asyncio.all_tasks(self._loop)
52
+ if task is not asyncio.current_task(self._loop)
53
+ ]
54
+
55
+ for task in tasks:
56
+ task.cancel()
57
+
58
+ await asyncio.gather(*tasks, return_exceptions=True)
59
+ self._loop.stop()
60
+
61
+ T = TypeVar("T")
62
+
63
+ def submit_coroutine(self, coro: Coroutine[Any, Any, T]) -> T:
64
+ """Submit a coroutine to the background event loop"""
65
+ if self._loop is None:
66
+ raise RuntimeError("Event loop not started")
67
+ return asyncio.run_coroutine_threadsafe(coro, self._loop).result()
@@ -28,7 +28,7 @@ dependencies = [
28
28
  ]
29
29
  requires-python = ">=3.9"
30
30
  readme = "README.md"
31
- version = "0.26.0a13"
31
+ version = "0.26.0a14"
32
32
 
33
33
  [project.license]
34
34
  text = "MIT"
@@ -7,8 +7,8 @@ import pytest
7
7
  import sqlalchemy as sa
8
8
 
9
9
  # Public API
10
- from dbos import DBOS, SetWorkflowID
11
- from dbos._dbos import WorkflowHandleAsync
10
+ from dbos import DBOS, Queue, SetWorkflowID
11
+ from dbos._dbos import WorkflowHandle, WorkflowHandleAsync
12
12
  from dbos._dbos_config import ConfigFile
13
13
  from dbos._error import DBOSException
14
14
 
@@ -56,6 +56,15 @@ async def test_async_workflow(dbos: DBOS) -> None:
56
56
  assert step_counter == 1
57
57
  assert txn_counter == 1
58
58
 
59
+ # Test DBOS.start_workflow_async
60
+ handle = await DBOS.start_workflow_async(test_workflow, "alice", "bob")
61
+ assert (await handle.get_result()) == "alicetxn21bobstep2"
62
+
63
+ # Test DBOS.start_workflow. Not recommended for async workflows,
64
+ # but needed for backwards compatibility.
65
+ sync_handle = DBOS.start_workflow(test_workflow, "alice", "bob")
66
+ assert sync_handle.get_result() == "alicetxn31bobstep3" # type: ignore
67
+
59
68
 
60
69
  @pytest.mark.asyncio
61
70
  async def test_async_step(dbos: DBOS) -> None:
@@ -160,10 +169,11 @@ async def test_send_recv_async(dbos: DBOS) -> None:
160
169
  none_uuid = str(uuid.uuid4())
161
170
  none_handle = None
162
171
  with SetWorkflowID(none_uuid):
163
- none_handle = dbos.start_workflow(test_recv_timeout, 10.0)
172
+ none_handle = await dbos.start_workflow_async(test_recv_timeout, 10.0)
164
173
  await test_send_none(none_uuid)
165
174
  begin_time = time.time()
166
- assert none_handle.get_result() is None
175
+ result = await none_handle.get_result() # type: ignore
176
+ assert result is None
167
177
  duration = time.time() - begin_time
168
178
  assert duration < 1.0 # None is from the received message, not from the timeout.
169
179
 
@@ -400,3 +410,59 @@ async def test_retrieve_workflow_async(dbos: DBOS) -> None:
400
410
  wfstatus = await handle.get_status()
401
411
  assert wfstatus.status == "SUCCESS"
402
412
  assert wfstatus.workflow_id == wfuuid
413
+
414
+
415
+ def test_unawaited_workflow(dbos: DBOS) -> None:
416
+ input = 5
417
+ child_id = str(uuid.uuid4())
418
+ queue = Queue("test_queue")
419
+
420
+ @DBOS.workflow()
421
+ async def child_workflow(x: int) -> int:
422
+ await asyncio.sleep(0.1)
423
+ return x
424
+
425
+ @DBOS.workflow()
426
+ async def parent_workflow(x: int) -> None:
427
+ with SetWorkflowID(child_id):
428
+ await DBOS.start_workflow_async(child_workflow, x)
429
+
430
+ assert queue.enqueue(parent_workflow, input).get_result() is None
431
+ handle: WorkflowHandle[int] = DBOS.retrieve_workflow(
432
+ child_id, existing_workflow=False
433
+ )
434
+ assert handle.get_result() == 5
435
+
436
+
437
+ def test_unawaited_workflow_exception(dbos: DBOS) -> None:
438
+ child_id = str(uuid.uuid4())
439
+ queue = Queue("test_queue")
440
+
441
+ @DBOS.workflow()
442
+ async def child_workflow(s: str) -> int:
443
+ await asyncio.sleep(0.1)
444
+ raise Exception(s)
445
+
446
+ @DBOS.workflow()
447
+ async def parent_workflow(s: str) -> None:
448
+ with SetWorkflowID(child_id):
449
+ await DBOS.start_workflow_async(child_workflow, s)
450
+
451
+ # Verify the unawaited child properly throws an exception
452
+ input = "alice"
453
+ assert queue.enqueue(parent_workflow, input).get_result() is None
454
+ handle: WorkflowHandle[int] = DBOS.retrieve_workflow(
455
+ child_id, existing_workflow=False
456
+ )
457
+ with pytest.raises(Exception) as exc_info:
458
+ handle.get_result()
459
+ assert input in str(exc_info.value)
460
+
461
+ # Verify it works if run again
462
+ input = "bob"
463
+ child_id = str(uuid.uuid4())
464
+ assert queue.enqueue(parent_workflow, input).get_result() is None
465
+ handle = DBOS.retrieve_workflow(child_id, existing_workflow=False)
466
+ with pytest.raises(Exception) as exc_info:
467
+ handle.get_result()
468
+ assert input in str(exc_info.value)
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