dbos 1.8.0a3__tar.gz → 1.8.0a5__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.
- {dbos-1.8.0a3 → dbos-1.8.0a5}/PKG-INFO +1 -1
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_client.py +15 -4
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_dbos.py +106 -2
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_dbos_config.py +24 -1
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_sys_db.py +3 -10
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/cli/cli.py +2 -2
- {dbos-1.8.0a3 → dbos-1.8.0a5}/pyproject.toml +1 -1
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/conftest.py +2 -1
- dbos-1.8.0a5/tests/test_async_workflow_management.py +264 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_config.py +16 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/LICENSE +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/README.md +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/__init__.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/__main__.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_admin_server.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_app_db.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_classproperty.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_conductor/conductor.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_conductor/protocol.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_context.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_core.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_croniter.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_debug.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_docker_pg_helper.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_error.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_event_loop.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_fastapi.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_flask.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_kafka.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_kafka_message.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_logger.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/env.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/script.py.mako +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/27ac6900c6ad_add_queue_dedup.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/66478e1b95e5_consolidate_queues.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/83f3732ae8e7_workflow_timeout.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/933e86bdac6a_add_queue_priority.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/d994145b47b6_consolidate_inputs.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_outcome.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_queue.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_recovery.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_registrations.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_roles.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_scheduler.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_schemas/__init__.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_schemas/application_database.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_schemas/system_database.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_serialization.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/README.md +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/__package/main.py.dbos +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_tracer.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_utils.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_workflow_commands.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/cli/_github_init.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/cli/_template_init.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/dbos-config.schema.json +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/py.typed +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/__init__.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/atexit_no_ctor.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/atexit_no_launch.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/classdefs.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/client_collateral.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/client_worker.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/dupname_classdefs1.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/dupname_classdefsa.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/more_classdefs.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/queuedworkflow.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_admin_server.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_async.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_classdecorators.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_cli.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_client.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_concurrency.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_croniter.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_dbos.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_debug.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_docker_secrets.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_failures.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_fastapi.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_fastapi_roles.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_flask.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_kafka.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_outcome.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_package.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_queue.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_scheduler.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_schema_migration.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_singleton.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_spans.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_sqlalchemy.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_workflow_introspection.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_workflow_management.py +0 -0
- {dbos-1.8.0a3 → dbos-1.8.0a5}/version/__init__.py +0 -0
|
@@ -13,7 +13,7 @@ else:
|
|
|
13
13
|
|
|
14
14
|
from dbos import _serialization
|
|
15
15
|
from dbos._dbos import WorkflowHandle, WorkflowHandleAsync
|
|
16
|
-
from dbos._dbos_config import is_valid_database_url
|
|
16
|
+
from dbos._dbos_config import get_system_database_url, is_valid_database_url
|
|
17
17
|
from dbos._error import DBOSException, DBOSNonExistentWorkflowError
|
|
18
18
|
from dbos._registrations import DEFAULT_MAX_RECOVERY_ATTEMPTS
|
|
19
19
|
from dbos._serialization import WorkflowInputs
|
|
@@ -97,17 +97,28 @@ class WorkflowHandleClientAsyncPolling(Generic[R]):
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
class DBOSClient:
|
|
100
|
-
def __init__(
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
database_url: str,
|
|
103
|
+
*,
|
|
104
|
+
system_database_url: Optional[str] = None,
|
|
105
|
+
system_database: Optional[str] = None,
|
|
106
|
+
):
|
|
101
107
|
assert is_valid_database_url(database_url)
|
|
102
108
|
# We only create database connections but do not run migrations
|
|
103
109
|
self._sys_db = SystemDatabase(
|
|
104
|
-
|
|
110
|
+
system_database_url=get_system_database_url(
|
|
111
|
+
{
|
|
112
|
+
"system_database_url": system_database_url,
|
|
113
|
+
"database_url": database_url,
|
|
114
|
+
"database": {"sys_db_name": system_database},
|
|
115
|
+
}
|
|
116
|
+
),
|
|
105
117
|
engine_kwargs={
|
|
106
118
|
"pool_timeout": 30,
|
|
107
119
|
"max_overflow": 0,
|
|
108
120
|
"pool_size": 2,
|
|
109
121
|
},
|
|
110
|
-
sys_db_name=system_database,
|
|
111
122
|
)
|
|
112
123
|
self._sys_db.check_connection()
|
|
113
124
|
self._app_db = ApplicationDatabase(
|
|
@@ -91,6 +91,7 @@ from ._context import (
|
|
|
91
91
|
from ._dbos_config import (
|
|
92
92
|
ConfigFile,
|
|
93
93
|
DBOSConfig,
|
|
94
|
+
get_system_database_url,
|
|
94
95
|
overwrite_config,
|
|
95
96
|
process_config,
|
|
96
97
|
translate_dbos_config_to_config_file,
|
|
@@ -424,9 +425,8 @@ class DBOS:
|
|
|
424
425
|
assert self._config["database_url"] is not None
|
|
425
426
|
assert self._config["database"]["sys_db_engine_kwargs"] is not None
|
|
426
427
|
self._sys_db_field = SystemDatabase(
|
|
427
|
-
|
|
428
|
+
system_database_url=get_system_database_url(self._config),
|
|
428
429
|
engine_kwargs=self._config["database"]["sys_db_engine_kwargs"],
|
|
429
|
-
sys_db_name=self._config["database"]["sys_db_name"],
|
|
430
430
|
debug_mode=debug_mode,
|
|
431
431
|
)
|
|
432
432
|
assert self._config["database"]["db_engine_kwargs"] is not None
|
|
@@ -966,6 +966,12 @@ class DBOS:
|
|
|
966
966
|
fn, "DBOS.cancelWorkflow"
|
|
967
967
|
)
|
|
968
968
|
|
|
969
|
+
@classmethod
|
|
970
|
+
async def cancel_workflow_async(cls, workflow_id: str) -> None:
|
|
971
|
+
"""Cancel a workflow by ID."""
|
|
972
|
+
await cls._configure_asyncio_thread_pool()
|
|
973
|
+
await asyncio.to_thread(cls.cancel_workflow, workflow_id)
|
|
974
|
+
|
|
969
975
|
@classmethod
|
|
970
976
|
async def _configure_asyncio_thread_pool(cls) -> None:
|
|
971
977
|
"""
|
|
@@ -987,11 +993,23 @@ class DBOS:
|
|
|
987
993
|
_get_dbos_instance()._sys_db.call_function_as_step(fn, "DBOS.resumeWorkflow")
|
|
988
994
|
return cls.retrieve_workflow(workflow_id)
|
|
989
995
|
|
|
996
|
+
@classmethod
|
|
997
|
+
async def resume_workflow_async(cls, workflow_id: str) -> WorkflowHandleAsync[Any]:
|
|
998
|
+
"""Resume a workflow by ID."""
|
|
999
|
+
await cls._configure_asyncio_thread_pool()
|
|
1000
|
+
await asyncio.to_thread(cls.resume_workflow, workflow_id)
|
|
1001
|
+
return await cls.retrieve_workflow_async(workflow_id)
|
|
1002
|
+
|
|
990
1003
|
@classmethod
|
|
991
1004
|
def restart_workflow(cls, workflow_id: str) -> WorkflowHandle[Any]:
|
|
992
1005
|
"""Restart a workflow with a new workflow ID"""
|
|
993
1006
|
return cls.fork_workflow(workflow_id, 1)
|
|
994
1007
|
|
|
1008
|
+
@classmethod
|
|
1009
|
+
async def restart_workflow_async(cls, workflow_id: str) -> WorkflowHandleAsync[Any]:
|
|
1010
|
+
"""Restart a workflow with a new workflow ID"""
|
|
1011
|
+
return await cls.fork_workflow_async(workflow_id, 1)
|
|
1012
|
+
|
|
995
1013
|
@classmethod
|
|
996
1014
|
def fork_workflow(
|
|
997
1015
|
cls,
|
|
@@ -1017,6 +1035,23 @@ class DBOS:
|
|
|
1017
1035
|
)
|
|
1018
1036
|
return cls.retrieve_workflow(new_id)
|
|
1019
1037
|
|
|
1038
|
+
@classmethod
|
|
1039
|
+
async def fork_workflow_async(
|
|
1040
|
+
cls,
|
|
1041
|
+
workflow_id: str,
|
|
1042
|
+
start_step: int,
|
|
1043
|
+
*,
|
|
1044
|
+
application_version: Optional[str] = None,
|
|
1045
|
+
) -> WorkflowHandleAsync[Any]:
|
|
1046
|
+
"""Restart a workflow with a new workflow ID from a specific step"""
|
|
1047
|
+
await cls._configure_asyncio_thread_pool()
|
|
1048
|
+
new_id = await asyncio.to_thread(
|
|
1049
|
+
lambda: cls.fork_workflow(
|
|
1050
|
+
workflow_id, start_step, application_version=application_version
|
|
1051
|
+
).get_workflow_id()
|
|
1052
|
+
)
|
|
1053
|
+
return await cls.retrieve_workflow_async(new_id)
|
|
1054
|
+
|
|
1020
1055
|
@classmethod
|
|
1021
1056
|
def list_workflows(
|
|
1022
1057
|
cls,
|
|
@@ -1057,6 +1092,42 @@ class DBOS:
|
|
|
1057
1092
|
fn, "DBOS.listWorkflows"
|
|
1058
1093
|
)
|
|
1059
1094
|
|
|
1095
|
+
@classmethod
|
|
1096
|
+
async def list_workflows_async(
|
|
1097
|
+
cls,
|
|
1098
|
+
*,
|
|
1099
|
+
workflow_ids: Optional[List[str]] = None,
|
|
1100
|
+
status: Optional[Union[str, List[str]]] = None,
|
|
1101
|
+
start_time: Optional[str] = None,
|
|
1102
|
+
end_time: Optional[str] = None,
|
|
1103
|
+
name: Optional[str] = None,
|
|
1104
|
+
app_version: Optional[str] = None,
|
|
1105
|
+
user: Optional[str] = None,
|
|
1106
|
+
limit: Optional[int] = None,
|
|
1107
|
+
offset: Optional[int] = None,
|
|
1108
|
+
sort_desc: bool = False,
|
|
1109
|
+
workflow_id_prefix: Optional[str] = None,
|
|
1110
|
+
load_input: bool = True,
|
|
1111
|
+
load_output: bool = True,
|
|
1112
|
+
) -> List[WorkflowStatus]:
|
|
1113
|
+
await cls._configure_asyncio_thread_pool()
|
|
1114
|
+
return await asyncio.to_thread(
|
|
1115
|
+
cls.list_workflows,
|
|
1116
|
+
workflow_ids=workflow_ids,
|
|
1117
|
+
status=status,
|
|
1118
|
+
start_time=start_time,
|
|
1119
|
+
end_time=end_time,
|
|
1120
|
+
name=name,
|
|
1121
|
+
app_version=app_version,
|
|
1122
|
+
user=user,
|
|
1123
|
+
limit=limit,
|
|
1124
|
+
offset=offset,
|
|
1125
|
+
sort_desc=sort_desc,
|
|
1126
|
+
workflow_id_prefix=workflow_id_prefix,
|
|
1127
|
+
load_input=load_input,
|
|
1128
|
+
load_output=load_output,
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1060
1131
|
@classmethod
|
|
1061
1132
|
def list_queued_workflows(
|
|
1062
1133
|
cls,
|
|
@@ -1089,6 +1160,34 @@ class DBOS:
|
|
|
1089
1160
|
fn, "DBOS.listQueuedWorkflows"
|
|
1090
1161
|
)
|
|
1091
1162
|
|
|
1163
|
+
@classmethod
|
|
1164
|
+
async def list_queued_workflows_async(
|
|
1165
|
+
cls,
|
|
1166
|
+
*,
|
|
1167
|
+
queue_name: Optional[str] = None,
|
|
1168
|
+
status: Optional[Union[str, List[str]]] = None,
|
|
1169
|
+
start_time: Optional[str] = None,
|
|
1170
|
+
end_time: Optional[str] = None,
|
|
1171
|
+
name: Optional[str] = None,
|
|
1172
|
+
limit: Optional[int] = None,
|
|
1173
|
+
offset: Optional[int] = None,
|
|
1174
|
+
sort_desc: bool = False,
|
|
1175
|
+
load_input: bool = True,
|
|
1176
|
+
) -> List[WorkflowStatus]:
|
|
1177
|
+
await cls._configure_asyncio_thread_pool()
|
|
1178
|
+
return await asyncio.to_thread(
|
|
1179
|
+
cls.list_queued_workflows,
|
|
1180
|
+
queue_name=queue_name,
|
|
1181
|
+
status=status,
|
|
1182
|
+
start_time=start_time,
|
|
1183
|
+
end_time=end_time,
|
|
1184
|
+
name=name,
|
|
1185
|
+
limit=limit,
|
|
1186
|
+
offset=offset,
|
|
1187
|
+
sort_desc=sort_desc,
|
|
1188
|
+
load_input=load_input,
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1092
1191
|
@classmethod
|
|
1093
1192
|
def list_workflow_steps(cls, workflow_id: str) -> List[StepInfo]:
|
|
1094
1193
|
def fn() -> List[StepInfo]:
|
|
@@ -1100,6 +1199,11 @@ class DBOS:
|
|
|
1100
1199
|
fn, "DBOS.listWorkflowSteps"
|
|
1101
1200
|
)
|
|
1102
1201
|
|
|
1202
|
+
@classmethod
|
|
1203
|
+
async def list_workflow_steps_async(cls, workflow_id: str) -> List[StepInfo]:
|
|
1204
|
+
await cls._configure_asyncio_thread_pool()
|
|
1205
|
+
return await asyncio.to_thread(cls.list_workflow_steps, workflow_id)
|
|
1206
|
+
|
|
1103
1207
|
@classproperty
|
|
1104
1208
|
def logger(cls) -> Logger:
|
|
1105
1209
|
"""Return the DBOS `Logger` for the current context."""
|
|
@@ -23,7 +23,8 @@ class DBOSConfig(TypedDict, total=False):
|
|
|
23
23
|
Attributes:
|
|
24
24
|
name (str): Application name
|
|
25
25
|
database_url (str): Database connection string
|
|
26
|
-
|
|
26
|
+
system_database_url (str): Connection string for the system database (if different from the application database)
|
|
27
|
+
sys_db_name (str): System database name (deprecated)
|
|
27
28
|
sys_db_pool_size (int): System database pool size
|
|
28
29
|
db_engine_kwargs (Dict[str, Any]): SQLAlchemy engine kwargs (See https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine)
|
|
29
30
|
log_level (str): Log level
|
|
@@ -36,6 +37,7 @@ class DBOSConfig(TypedDict, total=False):
|
|
|
36
37
|
|
|
37
38
|
name: str
|
|
38
39
|
database_url: Optional[str]
|
|
40
|
+
system_database_url: Optional[str]
|
|
39
41
|
sys_db_name: Optional[str]
|
|
40
42
|
sys_db_pool_size: Optional[int]
|
|
41
43
|
db_engine_kwargs: Optional[Dict[str, Any]]
|
|
@@ -111,6 +113,7 @@ class ConfigFile(TypedDict, total=False):
|
|
|
111
113
|
runtimeConfig: RuntimeConfig
|
|
112
114
|
database: DatabaseConfig
|
|
113
115
|
database_url: Optional[str]
|
|
116
|
+
system_database_url: Optional[str]
|
|
114
117
|
telemetry: Optional[TelemetryConfig]
|
|
115
118
|
env: Dict[str, str]
|
|
116
119
|
|
|
@@ -136,6 +139,8 @@ def translate_dbos_config_to_config_file(config: DBOSConfig) -> ConfigFile:
|
|
|
136
139
|
|
|
137
140
|
if "database_url" in config:
|
|
138
141
|
translated_config["database_url"] = config.get("database_url")
|
|
142
|
+
if "system_database_url" in config:
|
|
143
|
+
translated_config["system_database_url"] = config.get("system_database_url")
|
|
139
144
|
|
|
140
145
|
# Runtime config
|
|
141
146
|
translated_config["runtimeConfig"] = {"run_admin_server": True}
|
|
@@ -488,6 +493,8 @@ def overwrite_config(provided_config: ConfigFile) -> ConfigFile:
|
|
|
488
493
|
"DBOS_DATABASE_URL environment variable is not set. This is required to connect to the database."
|
|
489
494
|
)
|
|
490
495
|
provided_config["database_url"] = db_url
|
|
496
|
+
if "system_database_url" in provided_config:
|
|
497
|
+
del provided_config["system_database_url"]
|
|
491
498
|
|
|
492
499
|
# Telemetry config
|
|
493
500
|
if "telemetry" not in provided_config or provided_config["telemetry"] is None:
|
|
@@ -537,3 +544,19 @@ def overwrite_config(provided_config: ConfigFile) -> ConfigFile:
|
|
|
537
544
|
del provided_config["env"]
|
|
538
545
|
|
|
539
546
|
return provided_config
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def get_system_database_url(config: ConfigFile) -> str:
|
|
550
|
+
if "system_database_url" in config and config["system_database_url"] is not None:
|
|
551
|
+
return config["system_database_url"]
|
|
552
|
+
else:
|
|
553
|
+
assert config["database_url"] is not None
|
|
554
|
+
app_db_url = make_url(config["database_url"])
|
|
555
|
+
if config["database"].get("sys_db_name") is not None:
|
|
556
|
+
sys_db_name = config["database"]["sys_db_name"]
|
|
557
|
+
else:
|
|
558
|
+
assert app_db_url.database is not None
|
|
559
|
+
sys_db_name = app_db_url.database + SystemSchema.sysdb_suffix
|
|
560
|
+
return app_db_url.set(database=sys_db_name).render_as_string(
|
|
561
|
+
hide_password=False
|
|
562
|
+
)
|
|
@@ -331,22 +331,15 @@ class SystemDatabase:
|
|
|
331
331
|
def __init__(
|
|
332
332
|
self,
|
|
333
333
|
*,
|
|
334
|
-
|
|
334
|
+
system_database_url: str,
|
|
335
335
|
engine_kwargs: Dict[str, Any],
|
|
336
|
-
sys_db_name: Optional[str] = None,
|
|
337
336
|
debug_mode: bool = False,
|
|
338
337
|
):
|
|
339
338
|
# Set driver
|
|
340
|
-
|
|
341
|
-
# Resolve system database name
|
|
342
|
-
sysdb_name = sys_db_name
|
|
343
|
-
if not sysdb_name:
|
|
344
|
-
assert system_db_url.database is not None
|
|
345
|
-
sysdb_name = system_db_url.database + SystemSchema.sysdb_suffix
|
|
346
|
-
system_db_url = system_db_url.set(database=sysdb_name)
|
|
339
|
+
url = sa.make_url(system_database_url).set(drivername="postgresql+psycopg")
|
|
347
340
|
|
|
348
341
|
self.engine = sa.create_engine(
|
|
349
|
-
|
|
342
|
+
url,
|
|
350
343
|
**engine_kwargs,
|
|
351
344
|
)
|
|
352
345
|
self._engine_kwargs = engine_kwargs
|
|
@@ -21,6 +21,7 @@ from .._client import DBOSClient
|
|
|
21
21
|
from .._dbos_config import (
|
|
22
22
|
_app_name_to_db_name,
|
|
23
23
|
_is_valid_app_name,
|
|
24
|
+
get_system_database_url,
|
|
24
25
|
is_valid_database_url,
|
|
25
26
|
load_config,
|
|
26
27
|
)
|
|
@@ -294,13 +295,12 @@ def migrate(
|
|
|
294
295
|
sys_db = None
|
|
295
296
|
try:
|
|
296
297
|
sys_db = SystemDatabase(
|
|
297
|
-
|
|
298
|
+
system_database_url=get_system_database_url(config),
|
|
298
299
|
engine_kwargs={
|
|
299
300
|
"pool_timeout": 30,
|
|
300
301
|
"max_overflow": 0,
|
|
301
302
|
"pool_size": 2,
|
|
302
303
|
},
|
|
303
|
-
sys_db_name=sys_db_name,
|
|
304
304
|
)
|
|
305
305
|
app_db = ApplicationDatabase(
|
|
306
306
|
database_url=connection_string,
|
|
@@ -12,6 +12,7 @@ from flask import Flask
|
|
|
12
12
|
|
|
13
13
|
from dbos import DBOS, DBOSClient, DBOSConfig
|
|
14
14
|
from dbos._app_db import ApplicationDatabase
|
|
15
|
+
from dbos._dbos_config import get_system_database_url
|
|
15
16
|
from dbos._schemas.system_database import SystemSchema
|
|
16
17
|
from dbos._sys_db import SystemDatabase
|
|
17
18
|
|
|
@@ -40,7 +41,7 @@ def config() -> DBOSConfig:
|
|
|
40
41
|
def sys_db(config: DBOSConfig) -> Generator[SystemDatabase, Any, None]:
|
|
41
42
|
assert config["database_url"] is not None
|
|
42
43
|
sys_db = SystemDatabase(
|
|
43
|
-
|
|
44
|
+
system_database_url=f"{config['database_url']}_dbos_sys",
|
|
44
45
|
engine_kwargs={
|
|
45
46
|
"pool_timeout": 30,
|
|
46
47
|
"max_overflow": 0,
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from dbos import DBOS, Queue, SetWorkflowID
|
|
10
|
+
from dbos._error import DBOSWorkflowCancelledError
|
|
11
|
+
from dbos._sys_db import StepInfo, WorkflowStatus
|
|
12
|
+
from tests.conftest import queue_entries_are_cleaned_up
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.asyncio
|
|
16
|
+
async def test_cancel_workflow_async(dbos: DBOS) -> None:
|
|
17
|
+
"""Test async cancel_workflow method."""
|
|
18
|
+
steps_completed = 0
|
|
19
|
+
workflow_event = threading.Event()
|
|
20
|
+
main_thread_event = threading.Event()
|
|
21
|
+
input_val = 5
|
|
22
|
+
|
|
23
|
+
@DBOS.step()
|
|
24
|
+
def step_one() -> None:
|
|
25
|
+
nonlocal steps_completed
|
|
26
|
+
steps_completed += 1
|
|
27
|
+
|
|
28
|
+
@DBOS.step()
|
|
29
|
+
def step_two() -> None:
|
|
30
|
+
nonlocal steps_completed
|
|
31
|
+
steps_completed += 1
|
|
32
|
+
|
|
33
|
+
@DBOS.workflow()
|
|
34
|
+
def simple_workflow(x: int) -> int:
|
|
35
|
+
step_one()
|
|
36
|
+
main_thread_event.set()
|
|
37
|
+
workflow_event.wait()
|
|
38
|
+
step_two()
|
|
39
|
+
return x
|
|
40
|
+
|
|
41
|
+
# Start the workflow and cancel it async
|
|
42
|
+
wfid = str(uuid.uuid4())
|
|
43
|
+
with SetWorkflowID(wfid):
|
|
44
|
+
handle = DBOS.start_workflow(simple_workflow, input_val)
|
|
45
|
+
main_thread_event.wait()
|
|
46
|
+
await DBOS.cancel_workflow_async(wfid)
|
|
47
|
+
workflow_event.set()
|
|
48
|
+
|
|
49
|
+
with pytest.raises(DBOSWorkflowCancelledError):
|
|
50
|
+
handle.get_result()
|
|
51
|
+
assert steps_completed == 1
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.asyncio
|
|
55
|
+
async def test_resume_workflow_async(dbos: DBOS) -> None:
|
|
56
|
+
"""Test async resume_workflow method."""
|
|
57
|
+
steps_completed = 0
|
|
58
|
+
workflow_event = threading.Event()
|
|
59
|
+
main_thread_event = threading.Event()
|
|
60
|
+
input_val = 5
|
|
61
|
+
|
|
62
|
+
@DBOS.step()
|
|
63
|
+
def step_one() -> None:
|
|
64
|
+
nonlocal steps_completed
|
|
65
|
+
steps_completed += 1
|
|
66
|
+
|
|
67
|
+
@DBOS.step()
|
|
68
|
+
def step_two() -> None:
|
|
69
|
+
nonlocal steps_completed
|
|
70
|
+
steps_completed += 1
|
|
71
|
+
|
|
72
|
+
@DBOS.workflow()
|
|
73
|
+
def simple_workflow(x: int) -> int:
|
|
74
|
+
step_one()
|
|
75
|
+
main_thread_event.set()
|
|
76
|
+
workflow_event.wait()
|
|
77
|
+
step_two()
|
|
78
|
+
return x
|
|
79
|
+
|
|
80
|
+
# Start the workflow and cancel it
|
|
81
|
+
wfid = str(uuid.uuid4())
|
|
82
|
+
with SetWorkflowID(wfid):
|
|
83
|
+
handle = DBOS.start_workflow(simple_workflow, input_val)
|
|
84
|
+
main_thread_event.wait()
|
|
85
|
+
DBOS.cancel_workflow(wfid)
|
|
86
|
+
workflow_event.set()
|
|
87
|
+
|
|
88
|
+
with pytest.raises(DBOSWorkflowCancelledError):
|
|
89
|
+
handle.get_result()
|
|
90
|
+
assert steps_completed == 1
|
|
91
|
+
|
|
92
|
+
# Resume the workflow async
|
|
93
|
+
async_handle = await DBOS.resume_workflow_async(wfid)
|
|
94
|
+
assert (await async_handle.get_result()) == input_val
|
|
95
|
+
assert steps_completed == 2
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.asyncio
|
|
99
|
+
async def test_restart_workflow_async(dbos: DBOS) -> None:
|
|
100
|
+
"""Test async restart_workflow method."""
|
|
101
|
+
input_val = 2
|
|
102
|
+
multiplier = 5
|
|
103
|
+
|
|
104
|
+
@DBOS.workflow()
|
|
105
|
+
def simple_workflow(x: int) -> int:
|
|
106
|
+
return x * multiplier
|
|
107
|
+
|
|
108
|
+
# Start the workflow, let it finish, restart it async
|
|
109
|
+
handle = DBOS.start_workflow(simple_workflow, input_val)
|
|
110
|
+
assert handle.get_result() == input_val * multiplier
|
|
111
|
+
|
|
112
|
+
forked_handle = await DBOS.restart_workflow_async(handle.workflow_id)
|
|
113
|
+
assert forked_handle.workflow_id != handle.workflow_id
|
|
114
|
+
assert (await forked_handle.get_result()) == input_val * multiplier
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@pytest.mark.asyncio
|
|
118
|
+
async def test_fork_workflow_async(dbos: DBOS) -> None:
|
|
119
|
+
"""Test async fork_workflow method."""
|
|
120
|
+
step_one_count = 0
|
|
121
|
+
step_two_count = 0
|
|
122
|
+
step_three_count = 0
|
|
123
|
+
|
|
124
|
+
@DBOS.workflow()
|
|
125
|
+
def simple_workflow(x: int) -> int:
|
|
126
|
+
return step_one(x) + step_two(x) + step_three(x)
|
|
127
|
+
|
|
128
|
+
@DBOS.step()
|
|
129
|
+
def step_one(x: int) -> int:
|
|
130
|
+
nonlocal step_one_count
|
|
131
|
+
step_one_count += 1
|
|
132
|
+
return x + 1
|
|
133
|
+
|
|
134
|
+
@DBOS.step()
|
|
135
|
+
def step_two(x: int) -> int:
|
|
136
|
+
nonlocal step_two_count
|
|
137
|
+
step_two_count += 1
|
|
138
|
+
return x + 2
|
|
139
|
+
|
|
140
|
+
@DBOS.step()
|
|
141
|
+
def step_three(x: int) -> int:
|
|
142
|
+
nonlocal step_three_count
|
|
143
|
+
step_three_count += 1
|
|
144
|
+
return x + 3
|
|
145
|
+
|
|
146
|
+
input_val = 1
|
|
147
|
+
output = 3 * input_val + 6
|
|
148
|
+
|
|
149
|
+
wfid = str(uuid.uuid4())
|
|
150
|
+
with SetWorkflowID(wfid):
|
|
151
|
+
assert simple_workflow(input_val) == output
|
|
152
|
+
|
|
153
|
+
assert step_one_count == 1
|
|
154
|
+
assert step_two_count == 1
|
|
155
|
+
assert step_three_count == 1
|
|
156
|
+
|
|
157
|
+
# Fork from step 2 async
|
|
158
|
+
forked_handle = await DBOS.fork_workflow_async(wfid, 2)
|
|
159
|
+
assert forked_handle.workflow_id != wfid
|
|
160
|
+
assert (await forked_handle.get_result()) == output
|
|
161
|
+
|
|
162
|
+
# Verify step 1 didn't run again, but steps 2 and 3 did
|
|
163
|
+
assert step_one_count == 1
|
|
164
|
+
assert step_two_count == 2
|
|
165
|
+
assert step_three_count == 2
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@pytest.mark.asyncio
|
|
169
|
+
async def test_list_workflows_async(dbos: DBOS) -> None:
|
|
170
|
+
"""Test async list_workflows method."""
|
|
171
|
+
workflow_ids = []
|
|
172
|
+
|
|
173
|
+
@DBOS.workflow()
|
|
174
|
+
def simple_workflow(x: int) -> int:
|
|
175
|
+
return x
|
|
176
|
+
|
|
177
|
+
# Create a few workflows
|
|
178
|
+
for i in range(3):
|
|
179
|
+
wfid = str(uuid.uuid4())
|
|
180
|
+
workflow_ids.append(wfid)
|
|
181
|
+
with SetWorkflowID(wfid):
|
|
182
|
+
handle = DBOS.start_workflow(simple_workflow, i)
|
|
183
|
+
handle.get_result()
|
|
184
|
+
|
|
185
|
+
# List workflows async
|
|
186
|
+
workflows: List[WorkflowStatus] = await DBOS.list_workflows_async()
|
|
187
|
+
|
|
188
|
+
# Verify we have at least our workflows
|
|
189
|
+
assert len(workflows) >= 3
|
|
190
|
+
|
|
191
|
+
# Verify our workflow IDs are present
|
|
192
|
+
found_ids = {wf.workflow_id for wf in workflows}
|
|
193
|
+
for wfid in workflow_ids:
|
|
194
|
+
assert wfid in found_ids
|
|
195
|
+
|
|
196
|
+
# Test filtering by workflow_ids
|
|
197
|
+
filtered_workflows = await DBOS.list_workflows_async(workflow_ids=workflow_ids[:2])
|
|
198
|
+
assert len(filtered_workflows) == 2
|
|
199
|
+
assert all(wf.workflow_id in workflow_ids[:2] for wf in filtered_workflows)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@pytest.mark.asyncio
|
|
203
|
+
async def test_list_queued_workflows_async(dbos: DBOS) -> None:
|
|
204
|
+
"""Test async list_queued_workflows method."""
|
|
205
|
+
queue = Queue("test_queue_async")
|
|
206
|
+
workflow_event = threading.Event()
|
|
207
|
+
|
|
208
|
+
@DBOS.workflow()
|
|
209
|
+
def blocking_workflow(x: int) -> int:
|
|
210
|
+
workflow_event.wait()
|
|
211
|
+
return x
|
|
212
|
+
|
|
213
|
+
# Enqueue a workflow but don't let it complete yet
|
|
214
|
+
wfid = str(uuid.uuid4())
|
|
215
|
+
with SetWorkflowID(wfid):
|
|
216
|
+
handle = queue.enqueue(blocking_workflow, 42)
|
|
217
|
+
|
|
218
|
+
# List queued workflows async while workflow is still running
|
|
219
|
+
queued_workflows = await DBOS.list_queued_workflows_async(
|
|
220
|
+
queue_name="test_queue_async"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Verify our workflow is in the list
|
|
224
|
+
assert len(queued_workflows) >= 1
|
|
225
|
+
assert any(wf.workflow_id == wfid for wf in queued_workflows)
|
|
226
|
+
|
|
227
|
+
# Let the workflow complete
|
|
228
|
+
workflow_event.set()
|
|
229
|
+
handle.get_result()
|
|
230
|
+
|
|
231
|
+
assert queue_entries_are_cleaned_up(dbos)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@pytest.mark.asyncio
|
|
235
|
+
async def test_list_workflow_steps_async(dbos: DBOS) -> None:
|
|
236
|
+
"""Test async list_workflow_steps method."""
|
|
237
|
+
|
|
238
|
+
@DBOS.workflow()
|
|
239
|
+
def simple_workflow(x: int) -> int:
|
|
240
|
+
step_one(x)
|
|
241
|
+
step_two(x)
|
|
242
|
+
return x
|
|
243
|
+
|
|
244
|
+
@DBOS.step()
|
|
245
|
+
def step_one(x: int) -> int:
|
|
246
|
+
return x + 1
|
|
247
|
+
|
|
248
|
+
@DBOS.step()
|
|
249
|
+
def step_two(x: int) -> int:
|
|
250
|
+
return x + 2
|
|
251
|
+
|
|
252
|
+
wfid = str(uuid.uuid4())
|
|
253
|
+
with SetWorkflowID(wfid):
|
|
254
|
+
handle = DBOS.start_workflow(simple_workflow, 1)
|
|
255
|
+
handle.get_result()
|
|
256
|
+
|
|
257
|
+
# List workflow steps async
|
|
258
|
+
steps: List[StepInfo] = await DBOS.list_workflow_steps_async(wfid)
|
|
259
|
+
|
|
260
|
+
# Verify we have the expected steps (steps are returned as dictionaries)
|
|
261
|
+
assert len(steps) >= 2
|
|
262
|
+
step_names = {step["function_name"] for step in steps}
|
|
263
|
+
assert any("step_one" in name for name in step_names)
|
|
264
|
+
assert any("step_two" in name for name in step_names)
|
|
@@ -1135,6 +1135,22 @@ def test_configured_pool_default():
|
|
|
1135
1135
|
dbos.destroy()
|
|
1136
1136
|
|
|
1137
1137
|
|
|
1138
|
+
def test_configured_pool_custom_url():
|
|
1139
|
+
DBOS.destroy()
|
|
1140
|
+
config: DBOSConfig = {
|
|
1141
|
+
"name": "test-app",
|
|
1142
|
+
"database_url": f"postgres://postgres:{quote(os.environ.get('PGPASSWORD', 'dbos'))}@localhost:5432/postgres",
|
|
1143
|
+
"system_database_url": f"postgres://postgres:{quote(os.environ.get('PGPASSWORD', 'dbos'))}@localhost:5432/dbostesturl",
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
dbos = DBOS(config=config)
|
|
1147
|
+
dbos.launch()
|
|
1148
|
+
assert "postgres" in dbos._app_db.engine.url
|
|
1149
|
+
assert "dbostesturl" in dbos._sys_db.engine.url
|
|
1150
|
+
|
|
1151
|
+
dbos.destroy()
|
|
1152
|
+
|
|
1153
|
+
|
|
1138
1154
|
def test_configured_pool_user_provided():
|
|
1139
1155
|
DBOS.destroy()
|
|
1140
1156
|
config: DBOSConfig = {
|
|
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-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.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
|
|
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
|