dbos 0.22.0a5__tar.gz → 0.22.0a8__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-0.22.0a5 → dbos-0.22.0a8}/PKG-INFO +1 -1
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_admin_server.py +21 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_context.py +0 -1
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_core.py +1 -1
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_dbos.py +38 -5
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_logger.py +2 -1
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_recovery.py +6 -7
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_sys_db.py +11 -4
- {dbos-0.22.0a5 → dbos-0.22.0a8}/pyproject.toml +1 -1
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_admin_server.py +22 -1
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_dbos.py +157 -11
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_spans.py +4 -1
- {dbos-0.22.0a5 → dbos-0.22.0a8}/LICENSE +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/README.md +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/__init__.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_app_db.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_classproperty.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_cloudutils/authentication.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_cloudutils/cloudutils.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_cloudutils/databases.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_croniter.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_db_wizard.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_dbos_config.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_error.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_fastapi.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_flask.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_kafka.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_kafka_message.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_migrations/env.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_migrations/script.py.mako +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_outcome.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_queue.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_registrations.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_request.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_roles.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_scheduler.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_schemas/__init__.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_schemas/application_database.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_schemas/system_database.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_serialization.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_templates/dbos-db-starter/README.md +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_tracer.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/_workflow_commands.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/cli/_github_init.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/cli/_template_init.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/cli/cli.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/dbos-config.schema.json +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/dbos/py.typed +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/__init__.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/atexit_no_ctor.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/atexit_no_launch.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/classdefs.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/conftest.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/more_classdefs.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/queuedworkflow.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_async.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_classdecorators.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_concurrency.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_config.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_croniter.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_failures.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_fastapi.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_fastapi_roles.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_flask.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_kafka.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_outcome.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_package.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_queue.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_scheduler.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_schema_migration.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_singleton.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_sqlalchemy.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/tests/test_workflow_cmds.py +0 -0
- {dbos-0.22.0a5 → dbos-0.22.0a8}/version/__init__.py +0 -0
|
@@ -16,6 +16,7 @@ if TYPE_CHECKING:
|
|
|
16
16
|
_health_check_path = "/dbos-healthz"
|
|
17
17
|
_workflow_recovery_path = "/dbos-workflow-recovery"
|
|
18
18
|
_deactivate_path = "/deactivate"
|
|
19
|
+
_workflow_queues_metadata_path = "/dbos-workflow-queues-metadata"
|
|
19
20
|
# /workflows/:workflow_id/cancel
|
|
20
21
|
# /workflows/:workflow_id/resume
|
|
21
22
|
# /workflows/:workflow_id/restart
|
|
@@ -64,6 +65,26 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
|
|
|
64
65
|
self.send_response(200)
|
|
65
66
|
self._end_headers()
|
|
66
67
|
self.wfile.write("deactivated".encode("utf-8"))
|
|
68
|
+
elif self.path == _workflow_queues_metadata_path:
|
|
69
|
+
queue_metadata_array = []
|
|
70
|
+
from ._dbos import _get_or_create_dbos_registry
|
|
71
|
+
|
|
72
|
+
registry = _get_or_create_dbos_registry()
|
|
73
|
+
for queue in registry.queue_info_map.values():
|
|
74
|
+
queue_metadata = {
|
|
75
|
+
"name": queue.name,
|
|
76
|
+
"concurrency": queue.concurrency,
|
|
77
|
+
"workerConcurrency": queue.worker_concurrency,
|
|
78
|
+
"rateLimit": queue.limiter,
|
|
79
|
+
}
|
|
80
|
+
# Remove keys with None values
|
|
81
|
+
queue_metadata = {
|
|
82
|
+
k: v for k, v in queue_metadata.items() if v is not None
|
|
83
|
+
}
|
|
84
|
+
queue_metadata_array.append(queue_metadata)
|
|
85
|
+
self.send_response(200)
|
|
86
|
+
self._end_headers()
|
|
87
|
+
self.wfile.write(json.dumps(queue_metadata_array).encode("utf-8"))
|
|
67
88
|
else:
|
|
68
89
|
self.send_response(404)
|
|
69
90
|
self._end_headers()
|
|
@@ -49,7 +49,6 @@ class TracedAttributes(TypedDict, total=False):
|
|
|
49
49
|
class DBOSContext:
|
|
50
50
|
def __init__(self) -> None:
|
|
51
51
|
self.executor_id = os.environ.get("DBOS__VMID", "local")
|
|
52
|
-
self.app_version = os.environ.get("DBOS__APPVERSION", "")
|
|
53
52
|
self.app_id = os.environ.get("DBOS__APPID", "")
|
|
54
53
|
|
|
55
54
|
self.logger = dbos_logger
|
|
@@ -163,7 +163,7 @@ def _init_workflow(
|
|
|
163
163
|
"output": None,
|
|
164
164
|
"error": None,
|
|
165
165
|
"app_id": ctx.app_id,
|
|
166
|
-
"app_version":
|
|
166
|
+
"app_version": dbos.app_version,
|
|
167
167
|
"executor_id": ctx.executor_id,
|
|
168
168
|
"request": (
|
|
169
169
|
_serialization.serialize(ctx.request) if ctx.request is not None else None
|
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import atexit
|
|
5
|
+
import hashlib
|
|
6
|
+
import inspect
|
|
5
7
|
import json
|
|
6
8
|
import os
|
|
7
9
|
import sys
|
|
@@ -186,6 +188,22 @@ class DBOSRegistry:
|
|
|
186
188
|
else:
|
|
187
189
|
self.instance_info_map[fn] = inst
|
|
188
190
|
|
|
191
|
+
def compute_app_version(self) -> str:
|
|
192
|
+
"""
|
|
193
|
+
An application's version is computed from a hash of the source of its workflows.
|
|
194
|
+
This is guaranteed to be stable given identical source code because it uses an MD5 hash
|
|
195
|
+
and because it iterates through the workflows in sorted order.
|
|
196
|
+
This way, if the app's workflows are updated (which would break recovery), its version changes.
|
|
197
|
+
App version can be manually set through the DBOS__APPVERSION environment variable.
|
|
198
|
+
"""
|
|
199
|
+
hasher = hashlib.md5()
|
|
200
|
+
sources = sorted(
|
|
201
|
+
[inspect.getsource(wf) for wf in self.workflow_info_map.values()]
|
|
202
|
+
)
|
|
203
|
+
for source in sources:
|
|
204
|
+
hasher.update(source.encode("utf-8"))
|
|
205
|
+
return hasher.hexdigest()
|
|
206
|
+
|
|
189
207
|
|
|
190
208
|
class DBOS:
|
|
191
209
|
"""
|
|
@@ -283,6 +301,7 @@ class DBOS:
|
|
|
283
301
|
self._executor_field: Optional[ThreadPoolExecutor] = None
|
|
284
302
|
self._background_threads: List[threading.Thread] = []
|
|
285
303
|
self._executor_id: str = os.environ.get("DBOS__VMID", "local")
|
|
304
|
+
self.app_version: str = os.environ.get("DBOS__APPVERSION", "")
|
|
286
305
|
|
|
287
306
|
# If using FastAPI, set up middleware and lifecycle events
|
|
288
307
|
if self.fastapi is not None:
|
|
@@ -351,6 +370,10 @@ class DBOS:
|
|
|
351
370
|
dbos_logger.warning(f"DBOS was already launched")
|
|
352
371
|
return
|
|
353
372
|
self._launched = True
|
|
373
|
+
if self.app_version == "":
|
|
374
|
+
self.app_version = self._registry.compute_app_version()
|
|
375
|
+
dbos_logger.info(f"Application version: {self.app_version}")
|
|
376
|
+
dbos_tracer.app_version = self.app_version
|
|
354
377
|
self._executor_field = ThreadPoolExecutor(max_workers=64)
|
|
355
378
|
self._sys_db_field = SystemDatabase(self.config)
|
|
356
379
|
self._app_db_field = ApplicationDatabase(self.config)
|
|
@@ -359,9 +382,19 @@ class DBOS:
|
|
|
359
382
|
admin_port = 3001
|
|
360
383
|
self._admin_server_field = AdminServer(dbos=self, port=admin_port)
|
|
361
384
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
385
|
+
workflow_ids = self._sys_db.get_pending_workflows(
|
|
386
|
+
self._executor_id, self.app_version
|
|
387
|
+
)
|
|
388
|
+
if (len(workflow_ids)) > 0:
|
|
389
|
+
self.logger.info(
|
|
390
|
+
f"Recovering {len(workflow_ids)} workflows from application version {self.app_version}"
|
|
391
|
+
)
|
|
392
|
+
else:
|
|
393
|
+
self.logger.info(
|
|
394
|
+
f"No workflows to recover from application version {self.app_version}"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
self._executor.submit(startup_recovery_thread, self, workflow_ids)
|
|
365
398
|
|
|
366
399
|
# Listen to notifications
|
|
367
400
|
notification_listener_thread = threading.Thread(
|
|
@@ -398,13 +431,13 @@ class DBOS:
|
|
|
398
431
|
self._background_threads.append(poller_thread)
|
|
399
432
|
self._registry.pollers = []
|
|
400
433
|
|
|
401
|
-
dbos_logger.info("DBOS launched")
|
|
434
|
+
dbos_logger.info("DBOS launched!")
|
|
402
435
|
|
|
403
436
|
# Flush handlers and add OTLP to all loggers if enabled
|
|
404
437
|
# to enable their export in DBOS Cloud
|
|
405
438
|
for handler in dbos_logger.handlers:
|
|
406
439
|
handler.flush()
|
|
407
|
-
add_otlp_to_all_loggers()
|
|
440
|
+
add_otlp_to_all_loggers(self.app_version)
|
|
408
441
|
except Exception:
|
|
409
442
|
dbos_logger.error(f"DBOS failed to launch: {traceback.format_exc()}")
|
|
410
443
|
raise
|
|
@@ -86,8 +86,9 @@ def config_logger(config: "ConfigFile") -> None:
|
|
|
86
86
|
dbos_logger.addFilter(_otlp_transformer)
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
def add_otlp_to_all_loggers() -> None:
|
|
89
|
+
def add_otlp_to_all_loggers(app_version: str) -> None:
|
|
90
90
|
if _otlp_handler is not None and _otlp_transformer is not None:
|
|
91
|
+
_otlp_transformer.app_version = app_version
|
|
91
92
|
root = logging.root
|
|
92
93
|
|
|
93
94
|
root.addHandler(_otlp_handler)
|
|
@@ -43,12 +43,10 @@ def recover_pending_workflows(
|
|
|
43
43
|
) -> List["WorkflowHandle[Any]"]:
|
|
44
44
|
workflow_handles: List["WorkflowHandle[Any]"] = []
|
|
45
45
|
for executor_id in executor_ids:
|
|
46
|
-
if executor_id == "local" and os.environ.get("DBOS__VMID"):
|
|
47
|
-
dbos.logger.debug(
|
|
48
|
-
f"Skip local recovery because it's running in a VM: {os.environ.get('DBOS__VMID')}"
|
|
49
|
-
)
|
|
50
46
|
dbos.logger.debug(f"Recovering pending workflows for executor: {executor_id}")
|
|
51
|
-
pending_workflows = dbos._sys_db.get_pending_workflows(
|
|
47
|
+
pending_workflows = dbos._sys_db.get_pending_workflows(
|
|
48
|
+
executor_id, dbos.app_version
|
|
49
|
+
)
|
|
52
50
|
for pending_workflow in pending_workflows:
|
|
53
51
|
if (
|
|
54
52
|
pending_workflow.queue_name
|
|
@@ -65,6 +63,7 @@ def recover_pending_workflows(
|
|
|
65
63
|
workflow_handles.append(
|
|
66
64
|
execute_workflow_by_id(dbos, pending_workflow.workflow_uuid)
|
|
67
65
|
)
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
dbos.logger.info(
|
|
67
|
+
f"Recovering {len(pending_workflows)} workflows from version {dbos.app_version}"
|
|
68
|
+
)
|
|
70
69
|
return workflow_handles
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
import logging
|
|
2
3
|
import os
|
|
3
4
|
import re
|
|
4
5
|
import threading
|
|
@@ -13,6 +14,7 @@ from typing import (
|
|
|
13
14
|
Optional,
|
|
14
15
|
Sequence,
|
|
15
16
|
Set,
|
|
17
|
+
Tuple,
|
|
16
18
|
TypedDict,
|
|
17
19
|
cast,
|
|
18
20
|
)
|
|
@@ -22,8 +24,8 @@ import sqlalchemy as sa
|
|
|
22
24
|
import sqlalchemy.dialects.postgresql as pg
|
|
23
25
|
from alembic import command
|
|
24
26
|
from alembic.config import Config
|
|
25
|
-
from sqlalchemy import or_
|
|
26
27
|
from sqlalchemy.exc import DBAPIError
|
|
28
|
+
from sqlalchemy.sql import func
|
|
27
29
|
|
|
28
30
|
from . import _serialization
|
|
29
31
|
from ._dbos_config import ConfigFile
|
|
@@ -191,7 +193,7 @@ class SystemDatabase:
|
|
|
191
193
|
database="postgres",
|
|
192
194
|
# fills the "application_name" column in pg_stat_activity
|
|
193
195
|
query={
|
|
194
|
-
"application_name": f"dbos_transact_{os.environ.get('DBOS__VMID', 'local')}
|
|
196
|
+
"application_name": f"dbos_transact_{os.environ.get('DBOS__VMID', 'local')}"
|
|
195
197
|
},
|
|
196
198
|
)
|
|
197
199
|
engine = sa.create_engine(postgres_db_url)
|
|
@@ -213,7 +215,7 @@ class SystemDatabase:
|
|
|
213
215
|
database=sysdb_name,
|
|
214
216
|
# fills the "application_name" column in pg_stat_activity
|
|
215
217
|
query={
|
|
216
|
-
"application_name": f"dbos_transact_{os.environ.get('DBOS__VMID', 'local')}
|
|
218
|
+
"application_name": f"dbos_transact_{os.environ.get('DBOS__VMID', 'local')}"
|
|
217
219
|
},
|
|
218
220
|
)
|
|
219
221
|
|
|
@@ -228,6 +230,7 @@ class SystemDatabase:
|
|
|
228
230
|
)
|
|
229
231
|
alembic_cfg = Config()
|
|
230
232
|
alembic_cfg.set_main_option("script_location", migration_dir)
|
|
233
|
+
logging.getLogger("alembic").setLevel(logging.WARNING)
|
|
231
234
|
# Alembic requires the % in URL-escaped parameters to itself be escaped to %%.
|
|
232
235
|
escaped_conn_string = re.sub(
|
|
233
236
|
r"%(?=[0-9A-Fa-f]{2})",
|
|
@@ -307,6 +310,7 @@ class SystemDatabase:
|
|
|
307
310
|
recovery_attempts=(
|
|
308
311
|
SystemSchema.workflow_status.c.recovery_attempts + 1
|
|
309
312
|
),
|
|
313
|
+
updated_at=func.extract("epoch", func.now()) * 1000,
|
|
310
314
|
),
|
|
311
315
|
)
|
|
312
316
|
)
|
|
@@ -404,6 +408,7 @@ class SystemDatabase:
|
|
|
404
408
|
status=status["status"],
|
|
405
409
|
output=status["output"],
|
|
406
410
|
error=status["error"],
|
|
411
|
+
updated_at=func.extract("epoch", func.now()) * 1000,
|
|
407
412
|
),
|
|
408
413
|
)
|
|
409
414
|
)
|
|
@@ -767,7 +772,7 @@ class SystemDatabase:
|
|
|
767
772
|
return GetWorkflowsOutput(workflow_uuids)
|
|
768
773
|
|
|
769
774
|
def get_pending_workflows(
|
|
770
|
-
self, executor_id: str
|
|
775
|
+
self, executor_id: str, app_version: str
|
|
771
776
|
) -> list[GetPendingWorkflowsOutput]:
|
|
772
777
|
with self.engine.begin() as c:
|
|
773
778
|
rows = c.execute(
|
|
@@ -778,8 +783,10 @@ class SystemDatabase:
|
|
|
778
783
|
SystemSchema.workflow_status.c.status
|
|
779
784
|
== WorkflowStatusString.PENDING.value,
|
|
780
785
|
SystemSchema.workflow_status.c.executor_id == executor_id,
|
|
786
|
+
SystemSchema.workflow_status.c.application_version == app_version,
|
|
781
787
|
)
|
|
782
788
|
).fetchall()
|
|
789
|
+
|
|
783
790
|
return [
|
|
784
791
|
GetPendingWorkflowsOutput(
|
|
785
792
|
workflow_uuid=row.workflow_uuid,
|
|
@@ -5,7 +5,7 @@ import uuid
|
|
|
5
5
|
import requests
|
|
6
6
|
|
|
7
7
|
# Public API
|
|
8
|
-
from dbos import DBOS, ConfigFile, SetWorkflowID, _workflow_commands
|
|
8
|
+
from dbos import DBOS, ConfigFile, Queue, SetWorkflowID, _workflow_commands
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def test_admin_endpoints(dbos: DBOS) -> None:
|
|
@@ -23,6 +23,27 @@ def test_admin_endpoints(dbos: DBOS) -> None:
|
|
|
23
23
|
assert response.status_code == 200
|
|
24
24
|
assert response.json() == []
|
|
25
25
|
|
|
26
|
+
# Test GET /dbos-workflow-queues-metadata
|
|
27
|
+
Queue("q1")
|
|
28
|
+
Queue("q2", concurrency=1)
|
|
29
|
+
Queue("q3", concurrency=1, worker_concurrency=1)
|
|
30
|
+
Queue("q4", concurrency=1, worker_concurrency=1, limiter={"limit": 0, "period": 0})
|
|
31
|
+
response = requests.get(
|
|
32
|
+
"http://localhost:3001/dbos-workflow-queues-metadata", timeout=5
|
|
33
|
+
)
|
|
34
|
+
assert response.status_code == 200
|
|
35
|
+
assert response.json() == [
|
|
36
|
+
{"name": "q1"},
|
|
37
|
+
{"name": "q2", "concurrency": 1},
|
|
38
|
+
{"name": "q3", "concurrency": 1, "workerConcurrency": 1},
|
|
39
|
+
{
|
|
40
|
+
"name": "q4",
|
|
41
|
+
"concurrency": 1,
|
|
42
|
+
"workerConcurrency": 1,
|
|
43
|
+
"rateLimit": {"limit": 0, "period": 0},
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
|
|
26
47
|
# Test GET not found
|
|
27
48
|
response = requests.get("http://localhost:3001/stuff", timeout=5)
|
|
28
49
|
assert response.status_code == 404
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
# mypy: disable-error-code="no-redef"
|
|
2
|
+
|
|
1
3
|
import datetime
|
|
2
4
|
import logging
|
|
5
|
+
import os
|
|
3
6
|
import threading
|
|
4
7
|
import time
|
|
5
8
|
import uuid
|
|
@@ -71,14 +74,22 @@ def test_simple_workflow_attempts_counter(dbos: DBOS) -> None:
|
|
|
71
74
|
|
|
72
75
|
wfuuid = str(uuid.uuid4())
|
|
73
76
|
with dbos._sys_db.engine.connect() as c:
|
|
74
|
-
stmt = sa.select(
|
|
75
|
-
SystemSchema.workflow_status.c.
|
|
76
|
-
|
|
77
|
+
stmt = sa.select(
|
|
78
|
+
SystemSchema.workflow_status.c.recovery_attempts,
|
|
79
|
+
SystemSchema.workflow_status.c.created_at,
|
|
80
|
+
SystemSchema.workflow_status.c.updated_at,
|
|
81
|
+
).where(SystemSchema.workflow_status.c.workflow_uuid == wfuuid)
|
|
77
82
|
for i in range(10):
|
|
78
83
|
with SetWorkflowID(wfuuid):
|
|
79
84
|
noop()
|
|
80
|
-
result = c.execute(stmt).
|
|
81
|
-
assert result
|
|
85
|
+
result = c.execute(stmt).fetchone()
|
|
86
|
+
assert result is not None
|
|
87
|
+
recovery_attempts, created_at, updated_at = result
|
|
88
|
+
assert recovery_attempts == i + 1
|
|
89
|
+
if i == 0:
|
|
90
|
+
assert created_at == updated_at
|
|
91
|
+
else:
|
|
92
|
+
assert updated_at > created_at
|
|
82
93
|
|
|
83
94
|
|
|
84
95
|
def test_child_workflow(dbos: DBOS) -> None:
|
|
@@ -546,10 +557,13 @@ def test_recovery_temp_workflow(dbos: DBOS) -> None:
|
|
|
546
557
|
assert txn_counter == 1
|
|
547
558
|
|
|
548
559
|
|
|
549
|
-
def test_recovery_thread(config: ConfigFile
|
|
560
|
+
def test_recovery_thread(config: ConfigFile) -> None:
|
|
550
561
|
wf_counter: int = 0
|
|
551
562
|
test_var = "dbos"
|
|
552
563
|
|
|
564
|
+
DBOS.destroy(destroy_registry=True)
|
|
565
|
+
dbos = DBOS(config=config)
|
|
566
|
+
|
|
553
567
|
@DBOS.workflow()
|
|
554
568
|
def test_workflow(var: str) -> str:
|
|
555
569
|
nonlocal wf_counter
|
|
@@ -557,6 +571,8 @@ def test_recovery_thread(config: ConfigFile, dbos: DBOS) -> None:
|
|
|
557
571
|
wf_counter += 1
|
|
558
572
|
return var
|
|
559
573
|
|
|
574
|
+
DBOS.launch()
|
|
575
|
+
|
|
560
576
|
wfuuid = str(uuid.uuid4())
|
|
561
577
|
with SetWorkflowID(wfuuid):
|
|
562
578
|
assert test_workflow(test_var) == test_var
|
|
@@ -584,19 +600,19 @@ def test_recovery_thread(config: ConfigFile, dbos: DBOS) -> None:
|
|
|
584
600
|
}
|
|
585
601
|
)
|
|
586
602
|
|
|
587
|
-
|
|
588
|
-
|
|
603
|
+
DBOS.destroy(destroy_registry=True)
|
|
604
|
+
DBOS(config=config)
|
|
589
605
|
|
|
590
|
-
@DBOS.workflow()
|
|
606
|
+
@DBOS.workflow()
|
|
591
607
|
def test_workflow(var: str) -> str:
|
|
592
608
|
nonlocal wf_counter
|
|
593
609
|
if var == test_var:
|
|
594
610
|
wf_counter += 1
|
|
595
611
|
return var
|
|
596
612
|
|
|
597
|
-
DBOS.launch()
|
|
613
|
+
DBOS.launch()
|
|
598
614
|
|
|
599
|
-
# Upon re-
|
|
615
|
+
# Upon re-launch, the background thread should recover the workflow safely.
|
|
600
616
|
max_retries = 10
|
|
601
617
|
success = False
|
|
602
618
|
for i in range(max_retries):
|
|
@@ -1190,3 +1206,133 @@ def test_destroy_semantics(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
1190
1206
|
DBOS.launch()
|
|
1191
1207
|
|
|
1192
1208
|
assert test_workflow(var) == var
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
def test_app_version(config: ConfigFile) -> None:
|
|
1212
|
+
def is_hex(s: str) -> bool:
|
|
1213
|
+
return all(c in "0123456789abcdefABCDEF" for c in s)
|
|
1214
|
+
|
|
1215
|
+
DBOS.destroy(destroy_registry=True)
|
|
1216
|
+
dbos = DBOS(config=config)
|
|
1217
|
+
|
|
1218
|
+
@DBOS.workflow()
|
|
1219
|
+
def workflow_one(x: int) -> int:
|
|
1220
|
+
return x
|
|
1221
|
+
|
|
1222
|
+
@DBOS.workflow()
|
|
1223
|
+
def workflow_two(y: int) -> int:
|
|
1224
|
+
return y
|
|
1225
|
+
|
|
1226
|
+
DBOS.launch()
|
|
1227
|
+
|
|
1228
|
+
# Verify that app version is correctly set to a hex string
|
|
1229
|
+
app_version = dbos.app_version
|
|
1230
|
+
assert len(app_version) > 0
|
|
1231
|
+
assert is_hex(app_version)
|
|
1232
|
+
|
|
1233
|
+
DBOS.destroy(destroy_registry=True)
|
|
1234
|
+
dbos = DBOS(config=config)
|
|
1235
|
+
|
|
1236
|
+
@DBOS.workflow()
|
|
1237
|
+
def workflow_one(x: int) -> int:
|
|
1238
|
+
return x
|
|
1239
|
+
|
|
1240
|
+
@DBOS.workflow()
|
|
1241
|
+
def workflow_two(y: int) -> int:
|
|
1242
|
+
return y
|
|
1243
|
+
|
|
1244
|
+
DBOS.launch()
|
|
1245
|
+
|
|
1246
|
+
# Verify stability--the same workflow source produces the same app version.
|
|
1247
|
+
assert dbos.app_version == app_version
|
|
1248
|
+
|
|
1249
|
+
DBOS.destroy(destroy_registry=True)
|
|
1250
|
+
dbos = DBOS(config=config)
|
|
1251
|
+
|
|
1252
|
+
@DBOS.workflow()
|
|
1253
|
+
def workflow_one(x: int) -> int:
|
|
1254
|
+
return x
|
|
1255
|
+
|
|
1256
|
+
# Verify that changing the workflow source changes the workflow version
|
|
1257
|
+
DBOS.launch()
|
|
1258
|
+
assert dbos.app_version != app_version
|
|
1259
|
+
|
|
1260
|
+
# Verify that version can be overriden with an environment variable
|
|
1261
|
+
app_version = "12345"
|
|
1262
|
+
os.environ["DBOS__APPVERSION"] = app_version
|
|
1263
|
+
|
|
1264
|
+
DBOS.destroy(destroy_registry=True)
|
|
1265
|
+
dbos = DBOS(config=config)
|
|
1266
|
+
|
|
1267
|
+
@DBOS.workflow()
|
|
1268
|
+
def workflow_one(x: int) -> int:
|
|
1269
|
+
return x
|
|
1270
|
+
|
|
1271
|
+
DBOS.launch()
|
|
1272
|
+
assert dbos.app_version == app_version
|
|
1273
|
+
|
|
1274
|
+
del os.environ["DBOS__APPVERSION"]
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def test_recovery_appversion(config: ConfigFile) -> None:
|
|
1278
|
+
input = 5
|
|
1279
|
+
|
|
1280
|
+
DBOS.destroy(destroy_registry=True)
|
|
1281
|
+
dbos = DBOS(config=config)
|
|
1282
|
+
|
|
1283
|
+
@DBOS.workflow()
|
|
1284
|
+
def test_workflow(x: int) -> int:
|
|
1285
|
+
return x
|
|
1286
|
+
|
|
1287
|
+
DBOS.launch()
|
|
1288
|
+
|
|
1289
|
+
wfuuid = str(uuid.uuid4())
|
|
1290
|
+
with SetWorkflowID(wfuuid):
|
|
1291
|
+
assert test_workflow(input) == input
|
|
1292
|
+
|
|
1293
|
+
# Change the workflow status to pending
|
|
1294
|
+
dbos._sys_db.wait_for_buffer_flush()
|
|
1295
|
+
with dbos._sys_db.engine.begin() as c:
|
|
1296
|
+
c.execute(
|
|
1297
|
+
sa.update(SystemSchema.workflow_status)
|
|
1298
|
+
.values({"status": "PENDING", "name": test_workflow.__qualname__})
|
|
1299
|
+
.where(SystemSchema.workflow_status.c.workflow_uuid == wfuuid)
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
# Reconstruct an identical environment to simulate a restart
|
|
1303
|
+
DBOS.destroy(destroy_registry=True)
|
|
1304
|
+
dbos = DBOS(config=config)
|
|
1305
|
+
|
|
1306
|
+
@DBOS.workflow()
|
|
1307
|
+
def test_workflow(x: int) -> int:
|
|
1308
|
+
return x
|
|
1309
|
+
|
|
1310
|
+
DBOS.launch()
|
|
1311
|
+
|
|
1312
|
+
# The workflow should successfully recover
|
|
1313
|
+
workflow_handles = DBOS.recover_pending_workflows()
|
|
1314
|
+
assert len(workflow_handles) == 1
|
|
1315
|
+
assert workflow_handles[0].get_result() == input
|
|
1316
|
+
|
|
1317
|
+
# Change the workflow status to pending
|
|
1318
|
+
dbos._sys_db.wait_for_buffer_flush()
|
|
1319
|
+
with dbos._sys_db.engine.begin() as c:
|
|
1320
|
+
c.execute(
|
|
1321
|
+
sa.update(SystemSchema.workflow_status)
|
|
1322
|
+
.values({"status": "PENDING", "name": test_workflow.__qualname__})
|
|
1323
|
+
.where(SystemSchema.workflow_status.c.workflow_uuid == wfuuid)
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
# Now reconstruct a "modified application" with a different application version
|
|
1327
|
+
DBOS.destroy(destroy_registry=True)
|
|
1328
|
+
dbos = DBOS(config=config)
|
|
1329
|
+
|
|
1330
|
+
@DBOS.workflow()
|
|
1331
|
+
def test_workflow(x: int) -> int:
|
|
1332
|
+
return x + 1
|
|
1333
|
+
|
|
1334
|
+
DBOS.launch()
|
|
1335
|
+
|
|
1336
|
+
# The workflow should not recover
|
|
1337
|
+
workflow_handles = DBOS.recover_pending_workflows()
|
|
1338
|
+
assert len(workflow_handles) == 0
|
|
@@ -36,6 +36,7 @@ def test_spans(dbos: DBOS) -> None:
|
|
|
36
36
|
|
|
37
37
|
for span in spans:
|
|
38
38
|
assert span.attributes is not None
|
|
39
|
+
assert span.attributes["applicationVersion"] == dbos.app_version
|
|
39
40
|
assert span.context is not None
|
|
40
41
|
|
|
41
42
|
assert spans[0].name == test_step.__name__
|
|
@@ -73,6 +74,7 @@ async def test_spans_async(dbos: DBOS) -> None:
|
|
|
73
74
|
|
|
74
75
|
for span in spans:
|
|
75
76
|
assert span.attributes is not None
|
|
77
|
+
assert span.attributes["applicationVersion"] == dbos.app_version
|
|
76
78
|
assert span.context is not None
|
|
77
79
|
|
|
78
80
|
assert spans[0].name == test_step.__name__
|
|
@@ -85,7 +87,7 @@ async def test_spans_async(dbos: DBOS) -> None:
|
|
|
85
87
|
|
|
86
88
|
|
|
87
89
|
def test_temp_wf_fastapi(dbos_fastapi: Tuple[DBOS, FastAPI]) -> None:
|
|
88
|
-
|
|
90
|
+
dbos, app = dbos_fastapi
|
|
89
91
|
|
|
90
92
|
@app.get("/step")
|
|
91
93
|
@DBOS.step()
|
|
@@ -109,6 +111,7 @@ def test_temp_wf_fastapi(dbos_fastapi: Tuple[DBOS, FastAPI]) -> None:
|
|
|
109
111
|
|
|
110
112
|
for span in spans:
|
|
111
113
|
assert span.attributes is not None
|
|
114
|
+
assert span.attributes["applicationVersion"] == dbos.app_version
|
|
112
115
|
assert span.context is not None
|
|
113
116
|
|
|
114
117
|
assert spans[0].name == test_step_endpoint.__name__
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.22.0a5 → dbos-0.22.0a8}/dbos/_migrations/versions/5c361fc04708_added_system_tables.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
|