dbos 0.7.0a0__tar.gz → 0.7.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-0.7.0a0 → dbos-0.7.0a5}/PKG-INFO +5 -5
- {dbos-0.7.0a0 → dbos-0.7.0a5}/README.md +3 -3
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/application_database.py +6 -11
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/cli.py +6 -1
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/core.py +5 -5
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/dbos.py +1 -24
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/dbos_config.py +1 -1
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/fastapi.py +46 -2
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/scheduler/scheduler.py +1 -1
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/system_database.py +51 -67
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/dbos-config.yaml.dbos +1 -1
- {dbos-0.7.0a0 → dbos-0.7.0a5}/pyproject.toml +2 -3
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/conftest.py +1 -1
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/scheduler/test_scheduler.py +3 -3
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_failures.py +7 -7
- {dbos-0.7.0a0 → dbos-0.7.0a5}/LICENSE +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/__init__.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/admin_sever.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/context.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/dbos-config.schema.json +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/decorators.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/error.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/flask.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/kafka.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/kafka_message.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/logger.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/migrations/env.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/migrations/script.py.mako +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/migrations/versions/5c361fc04708_added_system_tables.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/py.typed +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/recovery.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/registrations.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/request.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/roles.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/scheduler/croniter.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/schemas/__init__.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/schemas/application_database.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/schemas/system_database.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/README.md +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/__package/__init__.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/__package/main.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/__package/schema.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/alembic.ini +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/migrations/env.py.dbos +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/migrations/script.py.mako +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/migrations/versions/2024_07_31_180642_init.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/start_postgres_docker.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/tracer.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/utils.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/__init__.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/atexit_no_ctor.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/atexit_no_launch.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/classdefs.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/more_classdefs.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/scheduler/test_croniter.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_admin_server.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_classdecorators.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_concurrency.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_config.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_dbos.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_fastapi.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_fastapi_roles.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_flask.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_kafka.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_package.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_schema_migration.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_singleton.py +0 -0
- {dbos-0.7.0a0 → dbos-0.7.0a5}/version/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dbos
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.0a5
|
|
4
4
|
Summary: Ultra-lightweight durable execution in Python
|
|
5
5
|
Author-Email: "DBOS, Inc." <contact@dbos.dev>
|
|
6
6
|
License: MIT
|
|
@@ -8,7 +8,6 @@ Requires-Python: >=3.9
|
|
|
8
8
|
Requires-Dist: pyyaml>=6.0.2
|
|
9
9
|
Requires-Dist: jsonschema>=4.23.0
|
|
10
10
|
Requires-Dist: alembic>=1.13.2
|
|
11
|
-
Requires-Dist: psycopg2-binary>=2.9.9
|
|
12
11
|
Requires-Dist: typing-extensions>=4.12.2; python_version < "3.10"
|
|
13
12
|
Requires-Dist: typer>=0.12.3
|
|
14
13
|
Requires-Dist: jsonpickle>=3.2.2
|
|
@@ -19,6 +18,7 @@ Requires-Dist: python-dateutil>=2.9.0.post0
|
|
|
19
18
|
Requires-Dist: fastapi[standard]>=0.112.1
|
|
20
19
|
Requires-Dist: psutil>=6.0.0
|
|
21
20
|
Requires-Dist: tomlkit>=0.13.2
|
|
21
|
+
Requires-Dist: psycopg>=3.2.1
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
24
24
|
|
|
@@ -91,7 +91,7 @@ def step_two():
|
|
|
91
91
|
print("Step two completed!")
|
|
92
92
|
|
|
93
93
|
@DBOS.workflow()
|
|
94
|
-
def
|
|
94
|
+
def dbos_workflow():
|
|
95
95
|
step_one()
|
|
96
96
|
for _ in range(5):
|
|
97
97
|
print("Press Control + \ to stop the app...")
|
|
@@ -99,8 +99,8 @@ def workflow():
|
|
|
99
99
|
step_two()
|
|
100
100
|
|
|
101
101
|
@app.get("/")
|
|
102
|
-
def
|
|
103
|
-
|
|
102
|
+
def fastapi_endpoint():
|
|
103
|
+
dbos_workflow()
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
Save the program into `main.py`, edit `dbos-config.yaml` to configure your Postgres connection settings, and start it with `fastapi run`.
|
|
@@ -68,7 +68,7 @@ def step_two():
|
|
|
68
68
|
print("Step two completed!")
|
|
69
69
|
|
|
70
70
|
@DBOS.workflow()
|
|
71
|
-
def
|
|
71
|
+
def dbos_workflow():
|
|
72
72
|
step_one()
|
|
73
73
|
for _ in range(5):
|
|
74
74
|
print("Press Control + \ to stop the app...")
|
|
@@ -76,8 +76,8 @@ def workflow():
|
|
|
76
76
|
step_two()
|
|
77
77
|
|
|
78
78
|
@app.get("/")
|
|
79
|
-
def
|
|
80
|
-
|
|
79
|
+
def fastapi_endpoint():
|
|
80
|
+
dbos_workflow()
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
Save the program into `main.py`, edit `dbos-config.yaml` to configure your Postgres connection settings, and start it with `fastapi run`.
|
|
@@ -2,7 +2,6 @@ from typing import Optional, TypedDict, cast
|
|
|
2
2
|
|
|
3
3
|
import sqlalchemy as sa
|
|
4
4
|
import sqlalchemy.dialects.postgresql as pg
|
|
5
|
-
import sqlalchemy.exc as sa_exc
|
|
6
5
|
from sqlalchemy.exc import DBAPIError
|
|
7
6
|
from sqlalchemy.orm import Session, sessionmaker
|
|
8
7
|
|
|
@@ -36,7 +35,7 @@ class ApplicationDatabase:
|
|
|
36
35
|
|
|
37
36
|
# If the application database does not already exist, create it
|
|
38
37
|
postgres_db_url = sa.URL.create(
|
|
39
|
-
"postgresql",
|
|
38
|
+
"postgresql+psycopg",
|
|
40
39
|
username=config["database"]["username"],
|
|
41
40
|
password=config["database"]["password"],
|
|
42
41
|
host=config["database"]["hostname"],
|
|
@@ -55,7 +54,7 @@ class ApplicationDatabase:
|
|
|
55
54
|
|
|
56
55
|
# Create a connection pool for the application database
|
|
57
56
|
app_db_url = sa.URL.create(
|
|
58
|
-
"postgresql",
|
|
57
|
+
"postgresql+psycopg",
|
|
59
58
|
username=config["database"]["username"],
|
|
60
59
|
password=config["database"]["password"],
|
|
61
60
|
host=config["database"]["hostname"],
|
|
@@ -97,11 +96,9 @@ class ApplicationDatabase:
|
|
|
97
96
|
)
|
|
98
97
|
)
|
|
99
98
|
except DBAPIError as dbapi_error:
|
|
100
|
-
if dbapi_error.orig.
|
|
99
|
+
if dbapi_error.orig.sqlstate == "23505": # type: ignore
|
|
101
100
|
raise DBOSWorkflowConflictIDError(output["workflow_uuid"])
|
|
102
|
-
raise
|
|
103
|
-
except Exception as e:
|
|
104
|
-
raise e
|
|
101
|
+
raise
|
|
105
102
|
|
|
106
103
|
def record_transaction_error(self, output: TransactionResultInternal) -> None:
|
|
107
104
|
try:
|
|
@@ -122,11 +119,9 @@ class ApplicationDatabase:
|
|
|
122
119
|
)
|
|
123
120
|
)
|
|
124
121
|
except DBAPIError as dbapi_error:
|
|
125
|
-
if dbapi_error.orig.
|
|
122
|
+
if dbapi_error.orig.sqlstate == "23505": # type: ignore
|
|
126
123
|
raise DBOSWorkflowConflictIDError(output["workflow_uuid"])
|
|
127
|
-
raise
|
|
128
|
-
except Exception as e:
|
|
129
|
-
raise e
|
|
124
|
+
raise
|
|
130
125
|
|
|
131
126
|
@staticmethod
|
|
132
127
|
def check_transaction_execution(
|
|
@@ -12,7 +12,7 @@ from typing import Any
|
|
|
12
12
|
import tomlkit
|
|
13
13
|
import typer
|
|
14
14
|
from rich import print
|
|
15
|
-
from rich.prompt import
|
|
15
|
+
from rich.prompt import Prompt
|
|
16
16
|
from typing_extensions import Annotated
|
|
17
17
|
|
|
18
18
|
from dbos import load_config
|
|
@@ -30,6 +30,7 @@ def on_windows() -> bool:
|
|
|
30
30
|
def start() -> None:
|
|
31
31
|
config = load_config()
|
|
32
32
|
start_commands = config["runtimeConfig"]["start"]
|
|
33
|
+
typer.echo("Executing start commands from 'dbos-config.yaml'")
|
|
33
34
|
for command in start_commands:
|
|
34
35
|
typer.echo(f"Executing: {command}")
|
|
35
36
|
|
|
@@ -129,9 +130,12 @@ def copy_template(src_dir: str, project_name: str, config_mode: bool) -> None:
|
|
|
129
130
|
"project_name": project_name,
|
|
130
131
|
"package_name": package_name,
|
|
131
132
|
"db_name": db_name,
|
|
133
|
+
"migration_command": "alembic upgrade head",
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
if config_mode:
|
|
137
|
+
ctx["package_name"] = "."
|
|
138
|
+
ctx["migration_command"] = "echo 'No migrations specified'"
|
|
135
139
|
copy_dbos_template(
|
|
136
140
|
os.path.join(src_dir, "dbos-config.yaml.dbos"),
|
|
137
141
|
os.path.join(dst_dir, "dbos-config.yaml"),
|
|
@@ -244,6 +248,7 @@ def migrate() -> None:
|
|
|
244
248
|
app_db.destroy()
|
|
245
249
|
|
|
246
250
|
# Next, run any custom migration commands specified in the configuration
|
|
251
|
+
typer.echo("Executing migration commands from 'dbos-config.yaml'")
|
|
247
252
|
try:
|
|
248
253
|
migrate_commands = (
|
|
249
254
|
config["database"]["migrate"]
|
|
@@ -192,7 +192,7 @@ def _execute_workflow(
|
|
|
192
192
|
status["status"] = "ERROR"
|
|
193
193
|
status["error"] = utils.serialize(error)
|
|
194
194
|
dbos.sys_db.update_workflow_status(status)
|
|
195
|
-
raise
|
|
195
|
+
raise
|
|
196
196
|
|
|
197
197
|
return output
|
|
198
198
|
|
|
@@ -217,7 +217,7 @@ def _execute_workflow_wthread(
|
|
|
217
217
|
dbos.logger.error(
|
|
218
218
|
f"Exception encountered in asynchronous workflow: {traceback.format_exc()}"
|
|
219
219
|
)
|
|
220
|
-
raise
|
|
220
|
+
raise
|
|
221
221
|
|
|
222
222
|
|
|
223
223
|
def _execute_workflow_id(dbos: "DBOS", workflow_id: str) -> "WorkflowHandle[Any]":
|
|
@@ -493,7 +493,7 @@ def _transaction(
|
|
|
493
493
|
)
|
|
494
494
|
break
|
|
495
495
|
except DBAPIError as dbapi_error:
|
|
496
|
-
if dbapi_error.orig.
|
|
496
|
+
if dbapi_error.orig.sqlstate == "40001": # type: ignore
|
|
497
497
|
# Retry on serialization failure
|
|
498
498
|
ctx.get_current_span().add_event(
|
|
499
499
|
"Transaction Serialization Failure",
|
|
@@ -505,13 +505,13 @@ def _transaction(
|
|
|
505
505
|
max_retry_wait_seconds,
|
|
506
506
|
)
|
|
507
507
|
continue
|
|
508
|
-
raise
|
|
508
|
+
raise
|
|
509
509
|
except Exception as error:
|
|
510
510
|
# Don't record the error if it was already recorded
|
|
511
511
|
if not has_recorded_error:
|
|
512
512
|
txn_output["error"] = utils.serialize(error)
|
|
513
513
|
dbos.app_db.record_transaction_error(txn_output)
|
|
514
|
-
raise
|
|
514
|
+
raise
|
|
515
515
|
return output
|
|
516
516
|
|
|
517
517
|
fi = get_or_create_func_info(func)
|
|
@@ -277,32 +277,9 @@ class DBOS:
|
|
|
277
277
|
|
|
278
278
|
# If using FastAPI, set up middleware and lifecycle events
|
|
279
279
|
if self.fastapi is not None:
|
|
280
|
-
from fastapi.requests import Request as FARequest
|
|
281
|
-
from fastapi.responses import JSONResponse
|
|
282
|
-
|
|
283
|
-
async def dbos_error_handler(
|
|
284
|
-
request: FARequest, gexc: Exception
|
|
285
|
-
) -> JSONResponse:
|
|
286
|
-
exc: DBOSException = cast(DBOSException, gexc)
|
|
287
|
-
status_code = 500
|
|
288
|
-
if exc.status_code is not None:
|
|
289
|
-
status_code = exc.status_code
|
|
290
|
-
return JSONResponse(
|
|
291
|
-
status_code=status_code,
|
|
292
|
-
content={
|
|
293
|
-
"message": str(exc.message),
|
|
294
|
-
"dbos_error_code": str(exc.dbos_error_code),
|
|
295
|
-
"dbos_error": str(exc.__class__.__name__),
|
|
296
|
-
},
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
self.fastapi.add_exception_handler(DBOSException, dbos_error_handler)
|
|
300
|
-
|
|
301
280
|
from dbos.fastapi import setup_fastapi_middleware
|
|
302
281
|
|
|
303
|
-
setup_fastapi_middleware(self.fastapi)
|
|
304
|
-
self.fastapi.on_event("startup")(self._launch)
|
|
305
|
-
self.fastapi.on_event("shutdown")(self._destroy)
|
|
282
|
+
setup_fastapi_middleware(self.fastapi, _get_dbos_instance())
|
|
306
283
|
|
|
307
284
|
# If using Flask, set up middleware
|
|
308
285
|
if self.flask is not None:
|
|
@@ -105,7 +105,7 @@ def get_dbos_database_url(config_file_path: str = "dbos-config.yaml") -> str:
|
|
|
105
105
|
"""
|
|
106
106
|
dbos_config = load_config(config_file_path)
|
|
107
107
|
db_url = URL.create(
|
|
108
|
-
"postgresql",
|
|
108
|
+
"postgresql+psycopg",
|
|
109
109
|
username=dbos_config["database"]["username"],
|
|
110
110
|
password=dbos_config["database"]["password"],
|
|
111
111
|
host=dbos_config["database"]["hostname"],
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import uuid
|
|
2
|
-
from typing import Any, Callable
|
|
2
|
+
from typing import Any, Callable, cast
|
|
3
3
|
|
|
4
4
|
from fastapi import FastAPI
|
|
5
5
|
from fastapi import Request as FastAPIRequest
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
7
|
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
|
8
|
+
|
|
9
|
+
from dbos import DBOS
|
|
10
|
+
from dbos.error import DBOSException
|
|
6
11
|
|
|
7
12
|
from .context import (
|
|
8
13
|
EnterDBOSHandler,
|
|
@@ -35,7 +40,46 @@ def make_request(request: FastAPIRequest) -> Request:
|
|
|
35
40
|
)
|
|
36
41
|
|
|
37
42
|
|
|
38
|
-
def
|
|
43
|
+
async def dbos_error_handler(request: FastAPIRequest, gexc: Exception) -> JSONResponse:
|
|
44
|
+
exc: DBOSException = cast(DBOSException, gexc)
|
|
45
|
+
status_code = 500
|
|
46
|
+
if exc.status_code is not None:
|
|
47
|
+
status_code = exc.status_code
|
|
48
|
+
return JSONResponse(
|
|
49
|
+
status_code=status_code,
|
|
50
|
+
content={
|
|
51
|
+
"message": str(exc.message),
|
|
52
|
+
"dbos_error_code": str(exc.dbos_error_code),
|
|
53
|
+
"dbos_error": str(exc.__class__.__name__),
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class LifespanMiddleware:
|
|
59
|
+
def __init__(self, app: ASGIApp, dbos: DBOS):
|
|
60
|
+
self.app = app
|
|
61
|
+
self.dbos = dbos
|
|
62
|
+
|
|
63
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
64
|
+
if scope["type"] == "lifespan":
|
|
65
|
+
while True:
|
|
66
|
+
message = await receive()
|
|
67
|
+
if message["type"] == "lifespan.startup":
|
|
68
|
+
self.dbos._launch()
|
|
69
|
+
await send({"type": "lifespan.startup.complete"})
|
|
70
|
+
elif message["type"] == "lifespan.shutdown":
|
|
71
|
+
self.dbos._destroy()
|
|
72
|
+
await send({"type": "lifespan.shutdown.complete"})
|
|
73
|
+
break
|
|
74
|
+
else:
|
|
75
|
+
await self.app(scope, receive, send)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def setup_fastapi_middleware(app: FastAPI, dbos: DBOS) -> None:
|
|
79
|
+
|
|
80
|
+
app.add_middleware(LifespanMiddleware, dbos=dbos)
|
|
81
|
+
app.add_exception_handler(DBOSException, dbos_error_handler)
|
|
82
|
+
|
|
39
83
|
@app.middleware("http")
|
|
40
84
|
async def dbos_fastapi_middleware(
|
|
41
85
|
request: FastAPIRequest, call_next: Callable[..., Any]
|
|
@@ -16,7 +16,7 @@ ScheduledWorkflow = Callable[[datetime, datetime], None]
|
|
|
16
16
|
def scheduler_loop(
|
|
17
17
|
func: ScheduledWorkflow, cron: str, stop_event: threading.Event
|
|
18
18
|
) -> None:
|
|
19
|
-
iter = croniter(cron, datetime.now(timezone.utc))
|
|
19
|
+
iter = croniter(cron, datetime.now(timezone.utc), second_at_beginning=True)
|
|
20
20
|
while not stop_event.is_set():
|
|
21
21
|
nextExecTime = iter.get_next(datetime)
|
|
22
22
|
sleepTime = nextExecTime - datetime.now(timezone.utc)
|
|
@@ -6,7 +6,7 @@ import time
|
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from typing import Any, Dict, List, Literal, Optional, Sequence, Set, TypedDict, cast
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import psycopg
|
|
10
10
|
import sqlalchemy as sa
|
|
11
11
|
import sqlalchemy.dialects.postgresql as pg
|
|
12
12
|
from alembic import command
|
|
@@ -154,7 +154,7 @@ class SystemDatabase:
|
|
|
154
154
|
|
|
155
155
|
# If the system database does not already exist, create it
|
|
156
156
|
postgres_db_url = sa.URL.create(
|
|
157
|
-
"postgresql",
|
|
157
|
+
"postgresql+psycopg",
|
|
158
158
|
username=config["database"]["username"],
|
|
159
159
|
password=config["database"]["password"],
|
|
160
160
|
host=config["database"]["hostname"],
|
|
@@ -172,7 +172,7 @@ class SystemDatabase:
|
|
|
172
172
|
engine.dispose()
|
|
173
173
|
|
|
174
174
|
system_db_url = sa.URL.create(
|
|
175
|
-
"postgresql",
|
|
175
|
+
"postgresql+psycopg",
|
|
176
176
|
username=config["database"]["username"],
|
|
177
177
|
password=config["database"]["password"],
|
|
178
178
|
host=config["database"]["hostname"],
|
|
@@ -196,7 +196,7 @@ class SystemDatabase:
|
|
|
196
196
|
)
|
|
197
197
|
command.upgrade(alembic_cfg, "head")
|
|
198
198
|
|
|
199
|
-
self.notification_conn: Optional[
|
|
199
|
+
self.notification_conn: Optional[psycopg.connection.Connection] = None
|
|
200
200
|
self.notifications_map: Dict[str, threading.Condition] = {}
|
|
201
201
|
self.workflow_events_map: Dict[str, threading.Condition] = {}
|
|
202
202
|
|
|
@@ -565,11 +565,9 @@ class SystemDatabase:
|
|
|
565
565
|
with self.engine.begin() as c:
|
|
566
566
|
c.execute(sql)
|
|
567
567
|
except DBAPIError as dbapi_error:
|
|
568
|
-
if dbapi_error.orig.
|
|
568
|
+
if dbapi_error.orig.sqlstate == "23505": # type: ignore
|
|
569
569
|
raise DBOSWorkflowConflictIDError(result["workflow_uuid"])
|
|
570
|
-
raise
|
|
571
|
-
except Exception as e:
|
|
572
|
-
raise e
|
|
570
|
+
raise
|
|
573
571
|
|
|
574
572
|
def check_operation_execution(
|
|
575
573
|
self, workflow_uuid: str, function_id: int, conn: Optional[sa.Connection] = None
|
|
@@ -623,11 +621,9 @@ class SystemDatabase:
|
|
|
623
621
|
)
|
|
624
622
|
except DBAPIError as dbapi_error:
|
|
625
623
|
# Foreign key violation
|
|
626
|
-
if dbapi_error.orig.
|
|
624
|
+
if dbapi_error.orig.sqlstate == "23503": # type: ignore
|
|
627
625
|
raise DBOSNonExistentWorkflowError(destination_uuid)
|
|
628
|
-
raise
|
|
629
|
-
except Exception as e:
|
|
630
|
-
raise e
|
|
626
|
+
raise
|
|
631
627
|
output: OperationResultInternal = {
|
|
632
628
|
"workflow_uuid": workflow_uuid,
|
|
633
629
|
"function_id": function_id,
|
|
@@ -729,69 +725,59 @@ class SystemDatabase:
|
|
|
729
725
|
return message
|
|
730
726
|
|
|
731
727
|
def _notification_listener(self) -> None:
|
|
732
|
-
notification_cursor: Optional[psycopg2.extensions.cursor] = None
|
|
733
728
|
while self._run_background_processes:
|
|
734
729
|
try:
|
|
735
|
-
#
|
|
736
|
-
|
|
737
|
-
self.engine.url.
|
|
730
|
+
# since we're using the psycopg connection directly, we need a url without the "+pycopg" suffix
|
|
731
|
+
url = sa.URL.create(
|
|
732
|
+
"postgresql", **self.engine.url.translate_connect_args()
|
|
738
733
|
)
|
|
739
|
-
|
|
740
|
-
|
|
734
|
+
# Listen to notifications
|
|
735
|
+
self.notification_conn = psycopg.connect(
|
|
736
|
+
url.render_as_string(hide_password=False), autocommit=True
|
|
741
737
|
)
|
|
742
|
-
notification_cursor = self.notification_conn.cursor()
|
|
743
738
|
|
|
744
|
-
|
|
745
|
-
|
|
739
|
+
self.notification_conn.execute("LISTEN dbos_notifications_channel")
|
|
740
|
+
self.notification_conn.execute("LISTEN dbos_workflow_events_channel")
|
|
741
|
+
|
|
746
742
|
while self._run_background_processes:
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
notify.payload
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
notify.payload
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
condition.acquire()
|
|
780
|
-
condition.notify_all()
|
|
781
|
-
condition.release()
|
|
782
|
-
dbos_logger.debug(
|
|
783
|
-
f"Signaled workflow_events condition for {notify.payload}"
|
|
784
|
-
)
|
|
785
|
-
else:
|
|
786
|
-
dbos_logger.error(f"Unknown channel: {channel}")
|
|
743
|
+
gen = self.notification_conn.notifies(timeout=60)
|
|
744
|
+
for notify in gen:
|
|
745
|
+
channel = notify.channel
|
|
746
|
+
dbos_logger.debug(
|
|
747
|
+
f"Received notification on channel: {channel}, payload: {notify.payload}"
|
|
748
|
+
)
|
|
749
|
+
if channel == "dbos_notifications_channel":
|
|
750
|
+
if (
|
|
751
|
+
notify.payload
|
|
752
|
+
and notify.payload in self.notifications_map
|
|
753
|
+
):
|
|
754
|
+
condition = self.notifications_map[notify.payload]
|
|
755
|
+
condition.acquire()
|
|
756
|
+
condition.notify_all()
|
|
757
|
+
condition.release()
|
|
758
|
+
dbos_logger.debug(
|
|
759
|
+
f"Signaled notifications condition for {notify.payload}"
|
|
760
|
+
)
|
|
761
|
+
elif channel == "dbos_workflow_events_channel":
|
|
762
|
+
if (
|
|
763
|
+
notify.payload
|
|
764
|
+
and notify.payload in self.workflow_events_map
|
|
765
|
+
):
|
|
766
|
+
condition = self.workflow_events_map[notify.payload]
|
|
767
|
+
condition.acquire()
|
|
768
|
+
condition.notify_all()
|
|
769
|
+
condition.release()
|
|
770
|
+
dbos_logger.debug(
|
|
771
|
+
f"Signaled workflow_events condition for {notify.payload}"
|
|
772
|
+
)
|
|
773
|
+
else:
|
|
774
|
+
dbos_logger.error(f"Unknown channel: {channel}")
|
|
787
775
|
except Exception as e:
|
|
788
776
|
if self._run_background_processes:
|
|
789
777
|
dbos_logger.error(f"Notification listener error: {e}")
|
|
790
778
|
time.sleep(1)
|
|
791
779
|
# Then the loop will try to reconnect and restart the listener
|
|
792
780
|
finally:
|
|
793
|
-
if notification_cursor is not None:
|
|
794
|
-
notification_cursor.close()
|
|
795
781
|
if self.notification_conn is not None:
|
|
796
782
|
self.notification_conn.close()
|
|
797
783
|
|
|
@@ -848,11 +834,9 @@ class SystemDatabase:
|
|
|
848
834
|
)
|
|
849
835
|
)
|
|
850
836
|
except DBAPIError as dbapi_error:
|
|
851
|
-
if dbapi_error.orig.
|
|
837
|
+
if dbapi_error.orig.sqlstate == "23505": # type: ignore
|
|
852
838
|
raise DBOSDuplicateWorkflowEventError(workflow_uuid, key)
|
|
853
|
-
raise
|
|
854
|
-
except Exception as e:
|
|
855
|
-
raise e
|
|
839
|
+
raise
|
|
856
840
|
output: OperationResultInternal = {
|
|
857
841
|
"workflow_uuid": workflow_uuid,
|
|
858
842
|
"function_id": function_id,
|
|
@@ -9,7 +9,6 @@ dependencies = [
|
|
|
9
9
|
"pyyaml>=6.0.2",
|
|
10
10
|
"jsonschema>=4.23.0",
|
|
11
11
|
"alembic>=1.13.2",
|
|
12
|
-
"psycopg2-binary>=2.9.9",
|
|
13
12
|
"typing-extensions>=4.12.2; python_version < \"3.10\"",
|
|
14
13
|
"typer>=0.12.3",
|
|
15
14
|
"jsonpickle>=3.2.2",
|
|
@@ -20,10 +19,11 @@ dependencies = [
|
|
|
20
19
|
"fastapi[standard]>=0.112.1",
|
|
21
20
|
"psutil>=6.0.0",
|
|
22
21
|
"tomlkit>=0.13.2",
|
|
22
|
+
"psycopg>=3.2.1",
|
|
23
23
|
]
|
|
24
24
|
requires-python = ">=3.9"
|
|
25
25
|
readme = "README.md"
|
|
26
|
-
version = "0.7.
|
|
26
|
+
version = "0.7.0a5"
|
|
27
27
|
|
|
28
28
|
[project.license]
|
|
29
29
|
text = "MIT"
|
|
@@ -58,7 +58,6 @@ dev = [
|
|
|
58
58
|
"requests>=2.32.3",
|
|
59
59
|
"types-requests>=2.32.0.20240712",
|
|
60
60
|
"httpx>=0.27.0",
|
|
61
|
-
"types-psycopg2>=2.9.21.20240417",
|
|
62
61
|
"pytz>=2024.1",
|
|
63
62
|
"GitPython>=3.1.43",
|
|
64
63
|
"confluent-kafka>=2.5.3",
|
|
@@ -48,7 +48,7 @@ def config() -> ConfigFile:
|
|
|
48
48
|
def postgres_db_engine() -> sa.Engine:
|
|
49
49
|
cfg = default_config()
|
|
50
50
|
postgres_db_url = sa.URL.create(
|
|
51
|
-
"postgresql",
|
|
51
|
+
"postgresql+psycopg",
|
|
52
52
|
username=cfg["database"]["username"],
|
|
53
53
|
password=cfg["database"]["password"],
|
|
54
54
|
host=cfg["database"]["hostname"],
|
|
@@ -9,14 +9,14 @@ from dbos import DBOS
|
|
|
9
9
|
def test_scheduled_workflow(dbos: DBOS) -> None:
|
|
10
10
|
wf_counter: int = 0
|
|
11
11
|
|
|
12
|
-
@DBOS.scheduled("
|
|
12
|
+
@DBOS.scheduled("*/2 * * * * *")
|
|
13
13
|
@DBOS.workflow()
|
|
14
14
|
def test_workflow(scheduled: datetime, actual: datetime) -> None:
|
|
15
15
|
nonlocal wf_counter
|
|
16
16
|
wf_counter += 1
|
|
17
17
|
|
|
18
|
-
time.sleep(
|
|
19
|
-
assert wf_counter
|
|
18
|
+
time.sleep(4)
|
|
19
|
+
assert wf_counter > 1 and wf_counter <= 3
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def test_scheduled_workflow_exception(dbos: DBOS) -> None:
|
|
@@ -4,7 +4,8 @@ import uuid
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
import sqlalchemy as sa
|
|
7
|
-
from
|
|
7
|
+
from psycopg.errors import SerializationFailure
|
|
8
|
+
from sqlalchemy.exc import OperationalError
|
|
8
9
|
|
|
9
10
|
# Public API
|
|
10
11
|
from dbos import DBOS, GetWorkflowsInput, SetWorkflowID
|
|
@@ -18,10 +19,9 @@ def test_transaction_errors(dbos: DBOS) -> None:
|
|
|
18
19
|
nonlocal retry_counter
|
|
19
20
|
if retry_counter < max_retry:
|
|
20
21
|
retry_counter += 1
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
raise err
|
|
22
|
+
raise OperationalError(
|
|
23
|
+
"Serialization test error", {}, SerializationFailure()
|
|
24
|
+
)
|
|
25
25
|
return max_retry
|
|
26
26
|
|
|
27
27
|
@DBOS.transaction()
|
|
@@ -36,7 +36,7 @@ def test_transaction_errors(dbos: DBOS) -> None:
|
|
|
36
36
|
|
|
37
37
|
with pytest.raises(Exception) as exc_info:
|
|
38
38
|
test_noretry_transaction()
|
|
39
|
-
assert exc_info.value.orig.
|
|
39
|
+
assert exc_info.value.orig.sqlstate == "42601" # type: ignore
|
|
40
40
|
assert retry_counter == 11
|
|
41
41
|
|
|
42
42
|
|
|
@@ -101,7 +101,7 @@ def test_buffer_flush_errors(dbos: DBOS) -> None:
|
|
|
101
101
|
# Crash the system database connection and make sure the buffer flush works on time.
|
|
102
102
|
backup_engine = dbos.sys_db.engine
|
|
103
103
|
dbos.sys_db.engine = sa.create_engine(
|
|
104
|
-
"postgresql+
|
|
104
|
+
"postgresql+psycopg://fake:database@localhost/fake_db"
|
|
105
105
|
)
|
|
106
106
|
|
|
107
107
|
res = test_transaction("bob")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/migrations/versions/2024_07_31_180642_init.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
|