dbos 0.26.0a11__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.
- {dbos-0.26.0a11 → dbos-0.26.0a14}/PKG-INFO +1 -1
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_core.py +3 -1
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_dbos.py +21 -9
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_docker_pg_helper.py +1 -1
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_error.py +51 -18
- dbos-0.26.0a14/dbos/_event_loop.py +67 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_registrations.py +19 -4
- {dbos-0.26.0a11 → dbos-0.26.0a14}/pyproject.toml +1 -1
- dbos-0.26.0a14/tests/dupname_classdefs1.py +9 -0
- dbos-0.26.0a14/tests/dupname_classdefsa.py +9 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_async.py +70 -4
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_classdecorators.py +37 -16
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_singleton.py +1 -1
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_workflow_management.py +5 -1
- {dbos-0.26.0a11 → dbos-0.26.0a14}/LICENSE +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/README.md +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/__init__.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/__main__.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_admin_server.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_app_db.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_classproperty.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_client.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_conductor/conductor.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_conductor/protocol.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_context.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_croniter.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_dbos_config.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_debug.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_fastapi.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_flask.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_kafka.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_kafka_message.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_logger.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_migrations/env.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_migrations/script.py.mako +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_outcome.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_queue.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_recovery.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_request.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_roles.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_scheduler.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_schemas/__init__.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_schemas/application_database.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_schemas/system_database.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_serialization.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_sys_db.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/README.md +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_tracer.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_utils.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_workflow_commands.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/cli/_github_init.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/cli/_template_init.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/cli/cli.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/dbos-config.schema.json +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/py.typed +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/__init__.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/atexit_no_ctor.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/atexit_no_launch.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/classdefs.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/client_collateral.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/client_worker.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/conftest.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/more_classdefs.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/queuedworkflow.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_admin_server.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_client.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_concurrency.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_config.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_croniter.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_dbos.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_debug.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_docker_secrets.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_failures.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_fastapi.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_fastapi_roles.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_flask.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_kafka.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_outcome.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_package.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_queue.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_scheduler.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_schema_migration.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_spans.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_sqlalchemy.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/tests/test_workflow_introspection.py +0 -0
- {dbos-0.26.0a11 → dbos-0.26.0a14}/version/__init__.py +0 -0
@@ -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
|
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 (
|
@@ -62,6 +60,7 @@ from ._recovery import recover_pending_workflows, startup_recovery_thread
|
|
62
60
|
from ._registrations import (
|
63
61
|
DEFAULT_MAX_RECOVERY_ATTEMPTS,
|
64
62
|
DBOSClassInfo,
|
63
|
+
_class_fqn,
|
65
64
|
get_or_create_class_info,
|
66
65
|
set_dbos_func_name,
|
67
66
|
set_temp_workflow_type,
|
@@ -111,6 +110,7 @@ from ._error import (
|
|
111
110
|
DBOSException,
|
112
111
|
DBOSNonExistentWorkflowError,
|
113
112
|
)
|
113
|
+
from ._event_loop import BackgroundEventLoop
|
114
114
|
from ._logger import add_otlp_to_all_loggers, config_logger, dbos_logger, init_logger
|
115
115
|
from ._sys_db import SystemDatabase
|
116
116
|
from ._workflow_commands import WorkflowStatus, get_workflow
|
@@ -181,15 +181,17 @@ class DBOSRegistry:
|
|
181
181
|
self.workflow_info_map[name] = wrapped_func
|
182
182
|
|
183
183
|
def register_class(self, cls: type, ci: DBOSClassInfo) -> None:
|
184
|
-
class_name = cls
|
184
|
+
class_name = _class_fqn(cls)
|
185
185
|
if class_name in self.class_info_map:
|
186
186
|
if self.class_info_map[class_name] is not cls:
|
187
187
|
raise Exception(f"Duplicate type registration for class '{class_name}'")
|
188
188
|
else:
|
189
189
|
self.class_info_map[class_name] = cls
|
190
190
|
|
191
|
-
def create_class_info(
|
192
|
-
|
191
|
+
def create_class_info(
|
192
|
+
self, cls: Type[T], class_name: Optional[str] = None
|
193
|
+
) -> Type[T]:
|
194
|
+
ci = get_or_create_class_info(cls, class_name)
|
193
195
|
self.register_class(cls, ci)
|
194
196
|
return cls
|
195
197
|
|
@@ -204,7 +206,7 @@ class DBOSRegistry:
|
|
204
206
|
|
205
207
|
def register_instance(self, inst: object) -> None:
|
206
208
|
config_name = getattr(inst, "config_name")
|
207
|
-
class_name = inst.__class__
|
209
|
+
class_name = _class_fqn(inst.__class__)
|
208
210
|
fn = f"{class_name}/{config_name}"
|
209
211
|
if fn in self.instance_info_map:
|
210
212
|
if self.instance_info_map[fn] is not inst:
|
@@ -338,6 +340,7 @@ class DBOS:
|
|
338
340
|
self.conductor_url: Optional[str] = conductor_url
|
339
341
|
self.conductor_key: Optional[str] = conductor_key
|
340
342
|
self.conductor_websocket: Optional[ConductorWebsocket] = None
|
343
|
+
self._background_event_loop: BackgroundEventLoop = BackgroundEventLoop()
|
341
344
|
|
342
345
|
init_logger()
|
343
346
|
|
@@ -448,6 +451,7 @@ class DBOS:
|
|
448
451
|
dbos_logger.info(f"Executor ID: {GlobalParams.executor_id}")
|
449
452
|
dbos_logger.info(f"Application version: {GlobalParams.app_version}")
|
450
453
|
self._executor_field = ThreadPoolExecutor(max_workers=64)
|
454
|
+
self._background_event_loop.start()
|
451
455
|
self._sys_db_field = SystemDatabase(
|
452
456
|
self.config["database"], debug_mode=debug_mode
|
453
457
|
)
|
@@ -565,6 +569,7 @@ class DBOS:
|
|
565
569
|
self._initialized = False
|
566
570
|
for event in self.stop_events:
|
567
571
|
event.set()
|
572
|
+
self._background_event_loop.stop()
|
568
573
|
if self._sys_db_field is not None:
|
569
574
|
self._sys_db_field.destroy()
|
570
575
|
self._sys_db_field = None
|
@@ -641,15 +646,22 @@ class DBOS:
|
|
641
646
|
)
|
642
647
|
|
643
648
|
@classmethod
|
644
|
-
def dbos_class(
|
649
|
+
def dbos_class(
|
650
|
+
cls, class_name: Optional[str] = None
|
651
|
+
) -> Callable[[Type[T]], Type[T]]:
|
645
652
|
"""
|
646
653
|
Decorate a class that contains DBOS member functions.
|
647
654
|
|
648
655
|
All DBOS classes must be decorated, as this associates the class with
|
649
|
-
its member functions.
|
656
|
+
its member functions. Class names must be globally unique. By default, the class name is class.__qualname__ but you can optionally provide a class name that is different from the default name.
|
650
657
|
"""
|
651
658
|
|
652
|
-
|
659
|
+
def register_class(cls: Type[T]) -> Type[T]:
|
660
|
+
# Register the class with the DBOS registry
|
661
|
+
_get_or_create_dbos_registry().create_class_info(cls, class_name)
|
662
|
+
return cls
|
663
|
+
|
664
|
+
return register_class
|
653
665
|
|
654
666
|
@classmethod
|
655
667
|
def default_required_roles(cls, roles: List[str]) -> Callable[[Type[T]], Type[T]]:
|
@@ -46,7 +46,7 @@ def start_docker_pg() -> None:
|
|
46
46
|
if has_docker:
|
47
47
|
start_docker_postgres(pool_config)
|
48
48
|
logging.info(
|
49
|
-
f"Postgres available at
|
49
|
+
f"Postgres available at postgresql://postgres:{pool_config['password']}@{pool_config['host']}:{pool_config['port']}"
|
50
50
|
)
|
51
51
|
else:
|
52
52
|
logging.warning("Docker not detected locally")
|
@@ -26,6 +26,29 @@ class DBOSException(Exception):
|
|
26
26
|
return f"DBOS Error: {self.message}"
|
27
27
|
|
28
28
|
|
29
|
+
class DBOSBaseException(BaseException):
|
30
|
+
"""
|
31
|
+
This class is for DBOS exceptions that should not be caught by user code.
|
32
|
+
It inherits from BaseException instead of Exception so it cannot be caught
|
33
|
+
except by code specifically trying to catch it.
|
34
|
+
|
35
|
+
Attributes:
|
36
|
+
message(str): The error message string
|
37
|
+
dbos_error_code(DBOSErrorCode): The error code, from the `DBOSErrorCode` enum
|
38
|
+
"""
|
39
|
+
|
40
|
+
def __init__(self, message: str, dbos_error_code: Optional[int] = None):
|
41
|
+
self.message = message
|
42
|
+
self.dbos_error_code = dbos_error_code
|
43
|
+
self.status_code: Optional[int] = None
|
44
|
+
super().__init__(self.message)
|
45
|
+
|
46
|
+
def __str__(self) -> str:
|
47
|
+
if self.dbos_error_code:
|
48
|
+
return f"DBOS Error {self.dbos_error_code}: {self.message}"
|
49
|
+
return f"DBOS Error: {self.message}"
|
50
|
+
|
51
|
+
|
29
52
|
class DBOSErrorCode(Enum):
|
30
53
|
ConflictingIDError = 1
|
31
54
|
RecoveryError = 2
|
@@ -41,14 +64,9 @@ class DBOSErrorCode(Enum):
|
|
41
64
|
ConflictingRegistrationError = 25
|
42
65
|
|
43
66
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
def __init__(self, workflow_id: str):
|
48
|
-
super().__init__(
|
49
|
-
f"Conflicting workflow ID {workflow_id}",
|
50
|
-
dbos_error_code=DBOSErrorCode.ConflictingIDError.value,
|
51
|
-
)
|
67
|
+
#######################################
|
68
|
+
## Exception
|
69
|
+
#######################################
|
52
70
|
|
53
71
|
|
54
72
|
class DBOSConflictingWorkflowError(DBOSException):
|
@@ -138,16 +156,6 @@ class DBOSMaxStepRetriesExceeded(DBOSException):
|
|
138
156
|
return (self.__class__, (self.step_name, self.max_retries))
|
139
157
|
|
140
158
|
|
141
|
-
class DBOSWorkflowCancelledError(DBOSException):
|
142
|
-
"""Exception raised when the workflow has already been cancelled."""
|
143
|
-
|
144
|
-
def __init__(self, msg: str) -> None:
|
145
|
-
super().__init__(
|
146
|
-
msg,
|
147
|
-
dbos_error_code=DBOSErrorCode.WorkflowCancelled.value,
|
148
|
-
)
|
149
|
-
|
150
|
-
|
151
159
|
class DBOSConflictingRegistrationError(DBOSException):
|
152
160
|
"""Exception raised when conflicting decorators are applied to the same function."""
|
153
161
|
|
@@ -168,3 +176,28 @@ class DBOSUnexpectedStepError(DBOSException):
|
|
168
176
|
f"During execution of workflow {workflow_id} step {step_id}, function {recorded_name} was recorded when {expected_name} was expected. Check that your workflow is deterministic.",
|
169
177
|
dbos_error_code=DBOSErrorCode.UnexpectedStep.value,
|
170
178
|
)
|
179
|
+
|
180
|
+
|
181
|
+
#######################################
|
182
|
+
## BaseException
|
183
|
+
#######################################
|
184
|
+
|
185
|
+
|
186
|
+
class DBOSWorkflowCancelledError(DBOSBaseException):
|
187
|
+
"""BaseException raised when the workflow has already been cancelled."""
|
188
|
+
|
189
|
+
def __init__(self, msg: str) -> None:
|
190
|
+
super().__init__(
|
191
|
+
msg,
|
192
|
+
dbos_error_code=DBOSErrorCode.WorkflowCancelled.value,
|
193
|
+
)
|
194
|
+
|
195
|
+
|
196
|
+
class DBOSWorkflowConflictIDError(DBOSBaseException):
|
197
|
+
"""BaseException raised when a workflow database record already exists."""
|
198
|
+
|
199
|
+
def __init__(self, workflow_id: str):
|
200
|
+
super().__init__(
|
201
|
+
f"Conflicting workflow ID {workflow_id}",
|
202
|
+
dbos_error_code=DBOSErrorCode.ConflictingIDError.value,
|
203
|
+
)
|
@@ -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()
|
@@ -34,6 +34,7 @@ def set_temp_workflow_type(f: Any, name: TempWorkflowType) -> None:
|
|
34
34
|
|
35
35
|
@dataclass
|
36
36
|
class DBOSClassInfo:
|
37
|
+
registered_name: str
|
37
38
|
def_required_roles: Optional[List[str]] = None
|
38
39
|
|
39
40
|
|
@@ -53,11 +54,17 @@ class DBOSFuncInfo:
|
|
53
54
|
max_recovery_attempts: int = DEFAULT_MAX_RECOVERY_ATTEMPTS
|
54
55
|
|
55
56
|
|
56
|
-
def get_or_create_class_info(
|
57
|
+
def get_or_create_class_info(
|
58
|
+
cls: Type[Any], provided_name: Optional[str] = None
|
59
|
+
) -> DBOSClassInfo:
|
57
60
|
if hasattr(cls, "dbos_class_decorator_info"):
|
58
61
|
ci: DBOSClassInfo = getattr(cls, "dbos_class_decorator_info")
|
59
62
|
return ci
|
60
|
-
|
63
|
+
class_name = _class_fqn(cls)
|
64
|
+
# Use the provided name instead of the class name if it is not None
|
65
|
+
if provided_name is not None:
|
66
|
+
class_name = provided_name
|
67
|
+
ci = DBOSClassInfo(registered_name=class_name)
|
61
68
|
setattr(cls, "dbos_class_decorator_info", ci)
|
62
69
|
|
63
70
|
# Tell all DBOS functions about this
|
@@ -166,16 +173,24 @@ def get_config_name(
|
|
166
173
|
return None
|
167
174
|
|
168
175
|
|
176
|
+
def _class_fqn(cls: type) -> str:
|
177
|
+
"""Returns the registered name of the given class. If the class name was not overridden at registration time, it returns the qualified name of the class."""
|
178
|
+
ci = get_class_info(cls)
|
179
|
+
if ci is not None:
|
180
|
+
return ci.registered_name
|
181
|
+
return cls.__qualname__
|
182
|
+
|
183
|
+
|
169
184
|
def get_dbos_class_name(
|
170
185
|
fi: Optional[DBOSFuncInfo], func: Callable[..., Any], args: Tuple[Any, ...]
|
171
186
|
) -> Optional[str]:
|
172
187
|
if fi and fi.func_type != DBOSFuncType.Unknown and len(args) > 0:
|
173
188
|
if fi.func_type == DBOSFuncType.Instance:
|
174
189
|
first_arg = args[0]
|
175
|
-
return
|
190
|
+
return _class_fqn(first_arg.__class__)
|
176
191
|
if fi.func_type == DBOSFuncType.Class:
|
177
192
|
first_arg = args[0]
|
178
|
-
return
|
193
|
+
return _class_fqn(first_arg)
|
179
194
|
return None
|
180
195
|
|
181
196
|
# Check for improperly-registered functions
|
@@ -0,0 +1,9 @@
|
|
1
|
+
from dbos import DBOS, DBOSConfiguredInstance
|
2
|
+
|
3
|
+
|
4
|
+
@DBOS.dbos_class(class_name="AnotherDBOSTestRegDup")
|
5
|
+
class DBOSTestRegDup(DBOSConfiguredInstance):
|
6
|
+
"""DBOSTestRegDup duplicates the name of a class defined dupname_classdefsa.py"""
|
7
|
+
|
8
|
+
def __init__(self, instance_name: str) -> None:
|
9
|
+
super().__init__(instance_name)
|
@@ -0,0 +1,9 @@
|
|
1
|
+
from dbos import DBOS, DBOSConfiguredInstance
|
2
|
+
|
3
|
+
|
4
|
+
@DBOS.dbos_class()
|
5
|
+
class DBOSTestRegDup(DBOSConfiguredInstance):
|
6
|
+
"""DBOSTestRegDup duplicates the name of a class defined dupname_classdefs1.py"""
|
7
|
+
|
8
|
+
def __init__(self, instance_name: str) -> None:
|
9
|
+
super().__init__(instance_name)
|
@@ -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.
|
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
|
-
|
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)
|
@@ -316,11 +316,11 @@ def test_simple_workflow_inst(dbos: DBOS) -> None:
|
|
316
316
|
stati = dbos.get_workflow_status(wfh.get_workflow_id())
|
317
317
|
assert stati
|
318
318
|
assert stati.config_name == "bob"
|
319
|
-
assert stati.class_name
|
319
|
+
assert stati.class_name and "DBOSTestClassInst" in stati.class_name
|
320
320
|
stat = wfh.get_status()
|
321
321
|
assert stat
|
322
322
|
assert stat.config_name == "bob"
|
323
|
-
assert stat.class_name
|
323
|
+
assert stat.class_name and "DBOSTestClassInst" in stat.class_name
|
324
324
|
|
325
325
|
assert wfh.get_result() == "bob1bob"
|
326
326
|
assert inst.txn_counter == 2
|
@@ -376,17 +376,38 @@ def test_duplicate_reg(dbos: DBOS) -> None:
|
|
376
376
|
def __init__(self) -> None:
|
377
377
|
super().__init__("bob")
|
378
378
|
|
379
|
-
assert
|
380
|
-
|
379
|
+
assert (
|
380
|
+
"Duplicate type registration for class 'test_duplicate_reg.<locals>.DBOSTestRegDup'"
|
381
|
+
== str(exc_info.value)
|
381
382
|
)
|
382
383
|
|
383
|
-
#
|
384
|
+
# Duplicate instance registration
|
384
385
|
inst = DBOSTestRegDup()
|
385
386
|
with pytest.raises(Exception) as exc_info:
|
386
387
|
inst = DBOSTestRegDup()
|
387
388
|
|
388
389
|
assert (
|
389
|
-
"Duplicate instance registration for class 'DBOSTestRegDup' instance 'bob'"
|
390
|
+
"Duplicate instance registration for class 'test_duplicate_reg.<locals>.DBOSTestRegDup' instance 'bob'"
|
391
|
+
== str(exc_info.value)
|
392
|
+
)
|
393
|
+
|
394
|
+
# There should be no collision when the duplicate class names are in
|
395
|
+
# different modules if they're specified with different names.
|
396
|
+
from tests import dupname_classdefs1, dupname_classdefsa
|
397
|
+
|
398
|
+
# Two instances of the same class may be registered if they have different
|
399
|
+
# instance_name.
|
400
|
+
# Duplicate instance registration error still occurs with identical class
|
401
|
+
# name and instance_name.
|
402
|
+
alice = dupname_classdefs1.DBOSTestRegDup("alice")
|
403
|
+
bob = dupname_classdefsa.DBOSTestRegDup("bob")
|
404
|
+
bob2 = dupname_classdefs1.DBOSTestRegDup("bob")
|
405
|
+
|
406
|
+
with pytest.raises(Exception) as exc_info:
|
407
|
+
bob2 = dupname_classdefs1.DBOSTestRegDup("bob")
|
408
|
+
|
409
|
+
assert (
|
410
|
+
"Duplicate instance registration for class 'AnotherDBOSTestRegDup' instance 'bob'"
|
390
411
|
== str(exc_info.value)
|
391
412
|
)
|
392
413
|
|
@@ -451,7 +472,7 @@ def test_inst_recovery(dbos: DBOS) -> None:
|
|
451
472
|
assert last_inst is None
|
452
473
|
|
453
474
|
status = DBOS.retrieve_workflow(wfid).get_status()
|
454
|
-
assert status.class_name
|
475
|
+
assert status.class_name and "TestClass" in status.class_name
|
455
476
|
assert status.config_name == "test_class"
|
456
477
|
|
457
478
|
|
@@ -479,7 +500,7 @@ def test_inst_async_recovery(dbos: DBOS) -> None:
|
|
479
500
|
orig_handle = DBOS.start_workflow(inst.workflow, input)
|
480
501
|
|
481
502
|
status = orig_handle.get_status()
|
482
|
-
assert status.class_name
|
503
|
+
assert status.class_name and "TestClass" in status.class_name
|
483
504
|
assert status.config_name == "test_class"
|
484
505
|
|
485
506
|
recovery_handle = DBOS._execute_workflow_id(wfid)
|
@@ -513,7 +534,7 @@ def test_inst_async_step_recovery(dbos: DBOS) -> None:
|
|
513
534
|
orig_handle = DBOS.start_workflow(inst.step, input)
|
514
535
|
|
515
536
|
status = orig_handle.get_status()
|
516
|
-
assert status.class_name
|
537
|
+
assert status.class_name and "TestClass" in status.class_name
|
517
538
|
assert status.config_name == "test_class"
|
518
539
|
|
519
540
|
recovery_handle = DBOS._execute_workflow_id(wfid)
|
@@ -558,7 +579,7 @@ def test_step_recovery(dbos: DBOS) -> None:
|
|
558
579
|
thread_event.wait()
|
559
580
|
|
560
581
|
status = DBOS.retrieve_workflow(wfid).get_status()
|
561
|
-
assert status.class_name
|
582
|
+
assert status.class_name and "TestClass" in status.class_name
|
562
583
|
assert status.config_name == "test_class"
|
563
584
|
|
564
585
|
recovery_handle = DBOS._execute_workflow_id(wfid)
|
@@ -626,7 +647,7 @@ def test_class_queue_recovery(dbos: DBOS) -> None:
|
|
626
647
|
result = [i * multiplier for i in range(5)]
|
627
648
|
for h in recovery_handles:
|
628
649
|
status = h.get_status()
|
629
|
-
assert status.class_name
|
650
|
+
assert status.class_name and "TestClass" in status.class_name
|
630
651
|
assert status.config_name == "test_class"
|
631
652
|
if h.get_workflow_id() == wfid:
|
632
653
|
assert h.get_result() == result
|
@@ -770,7 +791,7 @@ def test_class_classmethod_queue_recovery(dbos: DBOS) -> None:
|
|
770
791
|
for h in recovery_handles:
|
771
792
|
status = h.get_status()
|
772
793
|
# Class name is recorded for class methods
|
773
|
-
assert status.class_name
|
794
|
+
assert status.class_name and "TestClass" in status.class_name
|
774
795
|
assert status.config_name == None
|
775
796
|
if h.get_workflow_id() == wfid:
|
776
797
|
assert h.get_result() == result
|
@@ -808,13 +829,13 @@ def test_inst_txn(dbos: DBOS) -> None:
|
|
808
829
|
with SetWorkflowID(wfid):
|
809
830
|
assert inst.transaction(input) == input * multiplier
|
810
831
|
status = DBOS.retrieve_workflow(wfid).get_status()
|
811
|
-
assert status.class_name
|
832
|
+
assert status.class_name and "TestClass" in status.class_name
|
812
833
|
assert status.config_name == "test_class"
|
813
834
|
|
814
835
|
handle = DBOS.start_workflow(inst.transaction, input)
|
815
836
|
assert handle.get_result() == input * multiplier
|
816
837
|
status = handle.get_status()
|
817
|
-
assert status.class_name
|
838
|
+
assert status.class_name and "TestClass" in status.class_name
|
818
839
|
assert status.config_name == "test_class"
|
819
840
|
|
820
841
|
|
@@ -848,13 +869,13 @@ def test_mixed_methods(dbos: DBOS) -> None:
|
|
848
869
|
handle = DBOS.start_workflow(inst.instance_workflow, input)
|
849
870
|
assert handle.get_result() == input * multiplier
|
850
871
|
status = handle.get_status()
|
851
|
-
assert status.class_name
|
872
|
+
assert status.class_name and "TestClass" in status.class_name
|
852
873
|
assert status.config_name == "test_class"
|
853
874
|
|
854
875
|
handle = DBOS.start_workflow(inst.classmethod_workflow, input)
|
855
876
|
assert handle.get_result() == input
|
856
877
|
status = handle.get_status()
|
857
|
-
assert status.class_name
|
878
|
+
assert status.class_name and "TestClass" in status.class_name
|
858
879
|
assert status.config_name == None
|
859
880
|
|
860
881
|
handle = DBOS.start_workflow(inst.staticmethod_workflow, input)
|
@@ -48,7 +48,7 @@ def test_dbos_singleton(cleanup_test_databases: None) -> None:
|
|
48
48
|
stati = DBOS.get_workflow_status(wh.get_workflow_id())
|
49
49
|
assert stati
|
50
50
|
assert stati.config_name == "myconfig"
|
51
|
-
assert stati.class_name
|
51
|
+
assert stati.class_name and "DBOSTestClass" in stati.class_name
|
52
52
|
wfhr: WorkflowHandle[str] = DBOS.retrieve_workflow(wh.get_workflow_id())
|
53
53
|
assert wfhr.workflow_id == wh.get_workflow_id()
|
54
54
|
|
@@ -33,7 +33,11 @@ def test_cancel_resume(dbos: DBOS) -> None:
|
|
33
33
|
step_one()
|
34
34
|
main_thread_event.set()
|
35
35
|
workflow_event.wait()
|
36
|
-
|
36
|
+
# A handler like this should not catch DBOSWorkflowCancelledError
|
37
|
+
try:
|
38
|
+
step_two()
|
39
|
+
except Exception:
|
40
|
+
raise
|
37
41
|
return x
|
38
42
|
|
39
43
|
# Start the workflow and cancel it.
|
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
|
{dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py
RENAMED
File without changes
|
File without changes
|
{dbos-0.26.0a11 → dbos-0.26.0a14}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py
RENAMED
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
|