dbos 0.18.0__py3-none-any.whl → 0.19.0a4__py3-none-any.whl
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/_core.py +25 -7
- dbos/_db_wizard.py +48 -10
- dbos/_dbos.py +1 -1
- dbos/_dbos_config.py +52 -7
- dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +34 -0
- dbos/_queue.py +19 -0
- dbos/_schemas/system_database.py +1 -0
- dbos/_sys_db.py +33 -12
- dbos/_templates/hello/dbos-config.yaml.dbos +0 -4
- dbos/dbos-config.schema.json +2 -11
- {dbos-0.18.0.dist-info → dbos-0.19.0a4.dist-info}/METADATA +1 -1
- {dbos-0.18.0.dist-info → dbos-0.19.0a4.dist-info}/RECORD +15 -14
- {dbos-0.18.0.dist-info → dbos-0.19.0a4.dist-info}/WHEEL +0 -0
- {dbos-0.18.0.dist-info → dbos-0.19.0a4.dist-info}/entry_points.txt +0 -0
- {dbos-0.18.0.dist-info → dbos-0.19.0a4.dist-info}/licenses/LICENSE +0 -0
dbos/_core.py
CHANGED
|
@@ -84,7 +84,7 @@ if TYPE_CHECKING:
|
|
|
84
84
|
IsolationLevel,
|
|
85
85
|
)
|
|
86
86
|
|
|
87
|
-
from sqlalchemy.exc import DBAPIError
|
|
87
|
+
from sqlalchemy.exc import DBAPIError, InvalidRequestError
|
|
88
88
|
|
|
89
89
|
P = ParamSpec("P") # A generic type for workflow parameters
|
|
90
90
|
R = TypeVar("R", covariant=True) # A generic type for workflow return values
|
|
@@ -180,11 +180,12 @@ def _init_workflow(
|
|
|
180
180
|
if class_name is not None:
|
|
181
181
|
inputs = {"args": inputs["args"][1:], "kwargs": inputs["kwargs"]}
|
|
182
182
|
|
|
183
|
+
wf_status = status["status"]
|
|
183
184
|
if temp_wf_type != "transaction" or queue is not None:
|
|
184
185
|
# Synchronously record the status and inputs for workflows and single-step workflows
|
|
185
186
|
# We also have to do this for single-step workflows because of the foreign key constraint on the operation outputs table
|
|
186
187
|
# TODO: Make this transactional (and with the queue step below)
|
|
187
|
-
dbos._sys_db.update_workflow_status(
|
|
188
|
+
wf_status = dbos._sys_db.update_workflow_status(
|
|
188
189
|
status, False, ctx.in_recovery, max_recovery_attempts=max_recovery_attempts
|
|
189
190
|
)
|
|
190
191
|
dbos._sys_db.update_workflow_inputs(wfid, _serialization.serialize_args(inputs))
|
|
@@ -192,9 +193,10 @@ def _init_workflow(
|
|
|
192
193
|
# Buffer the inputs for single-transaction workflows, but don't buffer the status
|
|
193
194
|
dbos._sys_db.buffer_workflow_inputs(wfid, _serialization.serialize_args(inputs))
|
|
194
195
|
|
|
195
|
-
if queue is not None:
|
|
196
|
+
if queue is not None and wf_status == WorkflowStatusString.ENQUEUED.value:
|
|
196
197
|
dbos._sys_db.enqueue(wfid, queue)
|
|
197
198
|
|
|
199
|
+
status["status"] = wf_status
|
|
198
200
|
return status
|
|
199
201
|
|
|
200
202
|
|
|
@@ -413,7 +415,13 @@ def start_workflow(
|
|
|
413
415
|
max_recovery_attempts=fi.max_recovery_attempts,
|
|
414
416
|
)
|
|
415
417
|
|
|
416
|
-
|
|
418
|
+
wf_status = status["status"]
|
|
419
|
+
|
|
420
|
+
if (
|
|
421
|
+
not execute_workflow
|
|
422
|
+
or wf_status == WorkflowStatusString.ERROR.value
|
|
423
|
+
or wf_status == WorkflowStatusString.SUCCESS.value
|
|
424
|
+
):
|
|
417
425
|
return WorkflowHandlePolling(new_wf_id, dbos)
|
|
418
426
|
|
|
419
427
|
if fself is not None:
|
|
@@ -545,6 +553,7 @@ def decorate_transaction(
|
|
|
545
553
|
max_retry_wait_seconds = 2.0
|
|
546
554
|
while True:
|
|
547
555
|
has_recorded_error = False
|
|
556
|
+
txn_error: Optional[Exception] = None
|
|
548
557
|
try:
|
|
549
558
|
with session.begin():
|
|
550
559
|
# This must be the first statement in the transaction!
|
|
@@ -608,15 +617,24 @@ def decorate_transaction(
|
|
|
608
617
|
max_retry_wait_seconds,
|
|
609
618
|
)
|
|
610
619
|
continue
|
|
620
|
+
txn_error = dbapi_error
|
|
621
|
+
raise
|
|
622
|
+
except InvalidRequestError as invalid_request_error:
|
|
623
|
+
dbos.logger.error(
|
|
624
|
+
f"InvalidRequestError in transaction {func.__qualname__} \033[1m Hint: Do not call commit() or rollback() within a DBOS transaction.\033[0m"
|
|
625
|
+
)
|
|
626
|
+
txn_error = invalid_request_error
|
|
611
627
|
raise
|
|
612
628
|
except Exception as error:
|
|
629
|
+
txn_error = error
|
|
630
|
+
raise
|
|
631
|
+
finally:
|
|
613
632
|
# Don't record the error if it was already recorded
|
|
614
|
-
if not has_recorded_error:
|
|
633
|
+
if txn_error and not has_recorded_error:
|
|
615
634
|
txn_output["error"] = (
|
|
616
|
-
_serialization.serialize_exception(
|
|
635
|
+
_serialization.serialize_exception(txn_error)
|
|
617
636
|
)
|
|
618
637
|
dbos._app_db.record_transaction_error(txn_output)
|
|
619
|
-
raise
|
|
620
638
|
return output
|
|
621
639
|
|
|
622
640
|
if inspect.iscoroutinefunction(func):
|
dbos/_db_wizard.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
1
3
|
import time
|
|
2
|
-
from typing import TYPE_CHECKING, Optional
|
|
4
|
+
from typing import TYPE_CHECKING, Optional, TypedDict
|
|
3
5
|
|
|
4
6
|
import docker # type: ignore
|
|
5
7
|
import typer
|
|
@@ -15,8 +17,18 @@ from ._cloudutils.databases import choose_database, get_user_db_credentials
|
|
|
15
17
|
from ._error import DBOSInitializationError
|
|
16
18
|
from ._logger import dbos_logger
|
|
17
19
|
|
|
20
|
+
DB_CONNECTION_PATH = os.path.join(".dbos", "db_connection")
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
|
|
23
|
+
class DatabaseConnection(TypedDict):
|
|
24
|
+
hostname: Optional[str]
|
|
25
|
+
port: Optional[int]
|
|
26
|
+
username: Optional[str]
|
|
27
|
+
password: Optional[str]
|
|
28
|
+
local_suffix: Optional[bool]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def db_wizard(config: "ConfigFile", config_file_path: str) -> "ConfigFile":
|
|
20
32
|
# 1. Check the connectivity to the database. Return if successful. If cannot connect, continue to the following steps.
|
|
21
33
|
db_connection_error = _check_db_connectivity(config)
|
|
22
34
|
if db_connection_error is None:
|
|
@@ -82,17 +94,20 @@ def db_connect(config: "ConfigFile", config_file_path: str) -> "ConfigFile":
|
|
|
82
94
|
f"Could not connect to the database. Exception: {db_connection_error}"
|
|
83
95
|
)
|
|
84
96
|
|
|
85
|
-
# 6. Save the config to the
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
# 6. Save the config to the database connection file
|
|
98
|
+
updated_connection = DatabaseConnection(
|
|
99
|
+
hostname=config["database"]["hostname"],
|
|
100
|
+
port=config["database"]["port"],
|
|
101
|
+
username=config["database"]["username"],
|
|
102
|
+
password=config["database"]["password"],
|
|
103
|
+
local_suffix=config["database"]["local_suffix"],
|
|
104
|
+
)
|
|
105
|
+
save_db_connection(updated_connection)
|
|
90
106
|
return config
|
|
91
107
|
|
|
92
108
|
|
|
93
109
|
def _start_docker_postgres(config: "ConfigFile") -> bool:
|
|
94
110
|
print("Starting a Postgres Docker container...")
|
|
95
|
-
config["database"]["password"] = "dbos"
|
|
96
111
|
client = docker.from_env()
|
|
97
112
|
pg_data = "/var/lib/postgresql/data"
|
|
98
113
|
container_name = "dbos-db"
|
|
@@ -122,7 +137,7 @@ def _start_docker_postgres(config: "ConfigFile") -> bool:
|
|
|
122
137
|
continue
|
|
123
138
|
print("[green]Postgres Docker container started successfully![/green]")
|
|
124
139
|
break
|
|
125
|
-
except
|
|
140
|
+
except:
|
|
126
141
|
attempts -= 1
|
|
127
142
|
time.sleep(1)
|
|
128
143
|
|
|
@@ -151,7 +166,7 @@ def _check_db_connectivity(config: "ConfigFile") -> Optional[Exception]:
|
|
|
151
166
|
host=config["database"]["hostname"],
|
|
152
167
|
port=config["database"]["port"],
|
|
153
168
|
database="postgres",
|
|
154
|
-
query={"connect_timeout": "
|
|
169
|
+
query={"connect_timeout": "1"},
|
|
155
170
|
)
|
|
156
171
|
postgres_db_engine = create_engine(postgres_db_url)
|
|
157
172
|
try:
|
|
@@ -168,3 +183,26 @@ def _check_db_connectivity(config: "ConfigFile") -> Optional[Exception]:
|
|
|
168
183
|
postgres_db_engine.dispose()
|
|
169
184
|
|
|
170
185
|
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def load_db_connection() -> DatabaseConnection:
|
|
189
|
+
try:
|
|
190
|
+
with open(DB_CONNECTION_PATH, "r") as f:
|
|
191
|
+
data = json.load(f)
|
|
192
|
+
return DatabaseConnection(
|
|
193
|
+
hostname=data.get("hostname", None),
|
|
194
|
+
port=data.get("port", None),
|
|
195
|
+
username=data.get("username", None),
|
|
196
|
+
password=data.get("password", None),
|
|
197
|
+
local_suffix=data.get("local_suffix", None),
|
|
198
|
+
)
|
|
199
|
+
except:
|
|
200
|
+
return DatabaseConnection(
|
|
201
|
+
hostname=None, port=None, username=None, password=None, local_suffix=None
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def save_db_connection(connection: DatabaseConnection) -> None:
|
|
206
|
+
os.makedirs(".dbos", exist_ok=True)
|
|
207
|
+
with open(DB_CONNECTION_PATH, "w") as f:
|
|
208
|
+
json.dump(connection, f)
|
dbos/_dbos.py
CHANGED
|
@@ -83,7 +83,7 @@ from ._context import (
|
|
|
83
83
|
)
|
|
84
84
|
from ._dbos_config import ConfigFile, load_config, set_env_vars
|
|
85
85
|
from ._error import DBOSException, DBOSNonExistentWorkflowError
|
|
86
|
-
from ._logger import add_otlp_to_all_loggers, dbos_logger
|
|
86
|
+
from ._logger import add_otlp_to_all_loggers, dbos_logger
|
|
87
87
|
from ._sys_db import SystemDatabase
|
|
88
88
|
|
|
89
89
|
# Most DBOS functions are just any callable F, so decorators / wrappers work on F
|
dbos/_dbos_config.py
CHANGED
|
@@ -6,12 +6,15 @@ from typing import Any, Dict, List, Optional, TypedDict, cast
|
|
|
6
6
|
|
|
7
7
|
import yaml
|
|
8
8
|
from jsonschema import ValidationError, validate
|
|
9
|
+
from rich import print
|
|
9
10
|
from sqlalchemy import URL
|
|
10
11
|
|
|
11
|
-
from ._db_wizard import
|
|
12
|
+
from ._db_wizard import db_wizard, load_db_connection
|
|
12
13
|
from ._error import DBOSInitializationError
|
|
13
14
|
from ._logger import config_logger, dbos_logger, init_logger
|
|
14
15
|
|
|
16
|
+
DBOS_CONFIG_PATH = "dbos-config.yaml"
|
|
17
|
+
|
|
15
18
|
|
|
16
19
|
class RuntimeConfig(TypedDict, total=False):
|
|
17
20
|
start: List[str]
|
|
@@ -23,7 +26,7 @@ class DatabaseConfig(TypedDict, total=False):
|
|
|
23
26
|
hostname: str
|
|
24
27
|
port: int
|
|
25
28
|
username: str
|
|
26
|
-
password:
|
|
29
|
+
password: str
|
|
27
30
|
connectionTimeoutMillis: Optional[int]
|
|
28
31
|
app_db_name: str
|
|
29
32
|
sys_db_name: Optional[str]
|
|
@@ -93,7 +96,7 @@ def _substitute_env_vars(content: str) -> str:
|
|
|
93
96
|
return re.sub(regex, replace_func, content)
|
|
94
97
|
|
|
95
98
|
|
|
96
|
-
def get_dbos_database_url(config_file_path: str =
|
|
99
|
+
def get_dbos_database_url(config_file_path: str = DBOS_CONFIG_PATH) -> str:
|
|
97
100
|
"""
|
|
98
101
|
Retrieve application database URL from configuration `.yaml` file.
|
|
99
102
|
|
|
@@ -119,7 +122,9 @@ def get_dbos_database_url(config_file_path: str = "dbos-config.yaml") -> str:
|
|
|
119
122
|
return db_url.render_as_string(hide_password=False)
|
|
120
123
|
|
|
121
124
|
|
|
122
|
-
def load_config(
|
|
125
|
+
def load_config(
|
|
126
|
+
config_file_path: str = DBOS_CONFIG_PATH, *, use_db_wizard: bool = True
|
|
127
|
+
) -> ConfigFile:
|
|
123
128
|
"""
|
|
124
129
|
Load the DBOS `ConfigFile` from the specified path (typically `dbos-config.yaml`).
|
|
125
130
|
|
|
@@ -151,6 +156,9 @@ def load_config(config_file_path: str = "dbos-config.yaml") -> ConfigFile:
|
|
|
151
156
|
except ValidationError as e:
|
|
152
157
|
raise DBOSInitializationError(f"Validation error: {e}")
|
|
153
158
|
|
|
159
|
+
if "database" not in data:
|
|
160
|
+
data["database"] = {}
|
|
161
|
+
|
|
154
162
|
if "name" not in data:
|
|
155
163
|
raise DBOSInitializationError(
|
|
156
164
|
f"dbos-config.yaml must specify an application name"
|
|
@@ -169,8 +177,6 @@ def load_config(config_file_path: str = "dbos-config.yaml") -> ConfigFile:
|
|
|
169
177
|
if "runtimeConfig" not in data or "start" not in data["runtimeConfig"]:
|
|
170
178
|
raise DBOSInitializationError(f"dbos-config.yaml must specify a start command")
|
|
171
179
|
|
|
172
|
-
data = cast(ConfigFile, data)
|
|
173
|
-
|
|
174
180
|
if not _is_valid_app_name(data["name"]):
|
|
175
181
|
raise DBOSInitializationError(
|
|
176
182
|
f'Invalid app name {data["name"]}. App names must be between 3 and 30 characters long and contain only lowercase letters, numbers, dashes, and underscores.'
|
|
@@ -179,10 +185,49 @@ def load_config(config_file_path: str = "dbos-config.yaml") -> ConfigFile:
|
|
|
179
185
|
if "app_db_name" not in data["database"]:
|
|
180
186
|
data["database"]["app_db_name"] = _app_name_to_db_name(data["name"])
|
|
181
187
|
|
|
188
|
+
# Load the DB connection file. Use its values for missing fields from dbos-config.yaml. Use defaults otherwise.
|
|
189
|
+
data = cast(ConfigFile, data)
|
|
190
|
+
db_connection = load_db_connection()
|
|
191
|
+
if data["database"].get("hostname"):
|
|
192
|
+
print(
|
|
193
|
+
"[bold blue]Loading database connection parameters from dbos-config.yaml[/bold blue]"
|
|
194
|
+
)
|
|
195
|
+
elif db_connection.get("hostname"):
|
|
196
|
+
print(
|
|
197
|
+
"[bold blue]Loading database connection parameters from .dbos/db_connection[/bold blue]"
|
|
198
|
+
)
|
|
199
|
+
else:
|
|
200
|
+
print(
|
|
201
|
+
"[bold blue]Using default database connection parameters (localhost)[/bold blue]"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
data["database"]["hostname"] = (
|
|
205
|
+
data["database"].get("hostname") or db_connection.get("hostname") or "localhost"
|
|
206
|
+
)
|
|
207
|
+
data["database"]["port"] = (
|
|
208
|
+
data["database"].get("port") or db_connection.get("port") or 5432
|
|
209
|
+
)
|
|
210
|
+
data["database"]["username"] = (
|
|
211
|
+
data["database"].get("username") or db_connection.get("username") or "postgres"
|
|
212
|
+
)
|
|
213
|
+
data["database"]["password"] = (
|
|
214
|
+
data["database"].get("password")
|
|
215
|
+
or db_connection.get("password")
|
|
216
|
+
or os.environ.get("PGPASSWORD")
|
|
217
|
+
or "dbos"
|
|
218
|
+
)
|
|
219
|
+
data["database"]["local_suffix"] = (
|
|
220
|
+
data["database"].get("local_suffix")
|
|
221
|
+
or db_connection.get("local_suffix")
|
|
222
|
+
or False
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Configure the DBOS logger
|
|
182
226
|
config_logger(data)
|
|
183
227
|
|
|
184
228
|
# Check the connectivity to the database and make sure it's properly configured
|
|
185
|
-
|
|
229
|
+
if use_db_wizard:
|
|
230
|
+
data = db_wizard(data, config_file_path)
|
|
186
231
|
|
|
187
232
|
if "local_suffix" in data["database"] and data["database"]["local_suffix"]:
|
|
188
233
|
data["database"]["app_db_name"] = f"{data['database']['app_db_name']}_local"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""workflow_queues_executor_id
|
|
2
|
+
|
|
3
|
+
Revision ID: 04ca4f231047
|
|
4
|
+
Revises: d76646551a6c
|
|
5
|
+
Create Date: 2025-01-15 15:05:08.043190
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, Union
|
|
10
|
+
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
from alembic import op
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = "04ca4f231047"
|
|
16
|
+
down_revision: Union[str, None] = "d76646551a6c"
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
op.add_column(
|
|
23
|
+
"workflow_queue",
|
|
24
|
+
sa.Column(
|
|
25
|
+
"executor_id",
|
|
26
|
+
sa.Text(),
|
|
27
|
+
nullable=True,
|
|
28
|
+
),
|
|
29
|
+
schema="dbos",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def downgrade() -> None:
|
|
34
|
+
op.drop_column("workflow_queue", "executor_id", schema="dbos")
|
dbos/_queue.py
CHANGED
|
@@ -2,6 +2,9 @@ import threading
|
|
|
2
2
|
import traceback
|
|
3
3
|
from typing import TYPE_CHECKING, Optional, TypedDict
|
|
4
4
|
|
|
5
|
+
from psycopg import errors
|
|
6
|
+
from sqlalchemy.exc import OperationalError
|
|
7
|
+
|
|
5
8
|
from ._core import P, R, execute_workflow_by_id, start_workflow
|
|
6
9
|
|
|
7
10
|
if TYPE_CHECKING:
|
|
@@ -33,9 +36,19 @@ class Queue:
|
|
|
33
36
|
name: str,
|
|
34
37
|
concurrency: Optional[int] = None,
|
|
35
38
|
limiter: Optional[QueueRateLimit] = None,
|
|
39
|
+
worker_concurrency: Optional[int] = None,
|
|
36
40
|
) -> None:
|
|
41
|
+
if (
|
|
42
|
+
worker_concurrency is not None
|
|
43
|
+
and concurrency is not None
|
|
44
|
+
and worker_concurrency > concurrency
|
|
45
|
+
):
|
|
46
|
+
raise ValueError(
|
|
47
|
+
"worker_concurrency must be less than or equal to concurrency"
|
|
48
|
+
)
|
|
37
49
|
self.name = name
|
|
38
50
|
self.concurrency = concurrency
|
|
51
|
+
self.worker_concurrency = worker_concurrency
|
|
39
52
|
self.limiter = limiter
|
|
40
53
|
from ._dbos import _get_or_create_dbos_registry
|
|
41
54
|
|
|
@@ -60,6 +73,12 @@ def queue_thread(stop_event: threading.Event, dbos: "DBOS") -> None:
|
|
|
60
73
|
wf_ids = dbos._sys_db.start_queued_workflows(queue, dbos._executor_id)
|
|
61
74
|
for id in wf_ids:
|
|
62
75
|
execute_workflow_by_id(dbos, id)
|
|
76
|
+
except OperationalError as e:
|
|
77
|
+
# Ignore serialization error
|
|
78
|
+
if not isinstance(e.orig, errors.SerializationFailure):
|
|
79
|
+
dbos.logger.warning(
|
|
80
|
+
f"Exception encountered in queue thread: {traceback.format_exc()}"
|
|
81
|
+
)
|
|
63
82
|
except Exception:
|
|
64
83
|
dbos.logger.warning(
|
|
65
84
|
f"Exception encountered in queue thread: {traceback.format_exc()}"
|
dbos/_schemas/system_database.py
CHANGED
dbos/_sys_db.py
CHANGED
|
@@ -13,7 +13,6 @@ from typing import (
|
|
|
13
13
|
Optional,
|
|
14
14
|
Sequence,
|
|
15
15
|
Set,
|
|
16
|
-
Tuple,
|
|
17
16
|
TypedDict,
|
|
18
17
|
cast,
|
|
19
18
|
)
|
|
@@ -23,12 +22,14 @@ import sqlalchemy as sa
|
|
|
23
22
|
import sqlalchemy.dialects.postgresql as pg
|
|
24
23
|
from alembic import command
|
|
25
24
|
from alembic.config import Config
|
|
25
|
+
from sqlalchemy import or_
|
|
26
26
|
from sqlalchemy.exc import DBAPIError
|
|
27
27
|
|
|
28
28
|
from . import _serialization
|
|
29
29
|
from ._dbos_config import ConfigFile
|
|
30
30
|
from ._error import (
|
|
31
31
|
DBOSDeadLetterQueueError,
|
|
32
|
+
DBOSException,
|
|
32
33
|
DBOSNonExistentWorkflowError,
|
|
33
34
|
DBOSWorkflowConflictIDError,
|
|
34
35
|
)
|
|
@@ -249,7 +250,9 @@ class SystemDatabase:
|
|
|
249
250
|
*,
|
|
250
251
|
conn: Optional[sa.Connection] = None,
|
|
251
252
|
max_recovery_attempts: int = DEFAULT_MAX_RECOVERY_ATTEMPTS,
|
|
252
|
-
) ->
|
|
253
|
+
) -> WorkflowStatuses:
|
|
254
|
+
wf_status: WorkflowStatuses = status["status"]
|
|
255
|
+
|
|
253
256
|
cmd = pg.insert(SystemSchema.workflow_status).values(
|
|
254
257
|
workflow_uuid=status["workflow_uuid"],
|
|
255
258
|
status=status["status"],
|
|
@@ -286,17 +289,19 @@ class SystemDatabase:
|
|
|
286
289
|
)
|
|
287
290
|
else:
|
|
288
291
|
cmd = cmd.on_conflict_do_nothing()
|
|
289
|
-
cmd = cmd.returning(SystemSchema.workflow_status.c.recovery_attempts) # type: ignore
|
|
292
|
+
cmd = cmd.returning(SystemSchema.workflow_status.c.recovery_attempts, SystemSchema.workflow_status.c.status) # type: ignore
|
|
290
293
|
|
|
291
294
|
if conn is not None:
|
|
292
295
|
results = conn.execute(cmd)
|
|
293
296
|
else:
|
|
294
297
|
with self.engine.begin() as c:
|
|
295
298
|
results = c.execute(cmd)
|
|
299
|
+
|
|
296
300
|
if in_recovery:
|
|
297
301
|
row = results.fetchone()
|
|
298
302
|
if row is not None:
|
|
299
303
|
recovery_attempts: int = row[0]
|
|
304
|
+
wf_status = row[1]
|
|
300
305
|
if recovery_attempts > max_recovery_attempts:
|
|
301
306
|
with self.engine.begin() as c:
|
|
302
307
|
c.execute(
|
|
@@ -328,6 +333,8 @@ class SystemDatabase:
|
|
|
328
333
|
if status["workflow_uuid"] in self._temp_txn_wf_ids:
|
|
329
334
|
self._exported_temp_txn_wf_status.add(status["workflow_uuid"])
|
|
330
335
|
|
|
336
|
+
return wf_status
|
|
337
|
+
|
|
331
338
|
def set_workflow_status(
|
|
332
339
|
self,
|
|
333
340
|
workflow_uuid: str,
|
|
@@ -405,7 +412,10 @@ class SystemDatabase:
|
|
|
405
412
|
res["output"]
|
|
406
413
|
)
|
|
407
414
|
return resstat
|
|
408
|
-
|
|
415
|
+
else:
|
|
416
|
+
raise DBOSException(
|
|
417
|
+
"Workflow status record not found. This should not happen! \033[1m Hint: Check if your workflow is deterministic.\033[0m"
|
|
418
|
+
)
|
|
409
419
|
stat = self.get_workflow_status(workflow_uuid)
|
|
410
420
|
self.record_operation_result(
|
|
411
421
|
{
|
|
@@ -1130,27 +1140,38 @@ class SystemDatabase:
|
|
|
1130
1140
|
if num_recent_queries >= queue.limiter["limit"]:
|
|
1131
1141
|
return []
|
|
1132
1142
|
|
|
1133
|
-
#
|
|
1134
|
-
#
|
|
1135
|
-
# If there is a concurrency limit N, select only the N most recent
|
|
1143
|
+
# Dequeue functions eligible for this worker and ordered by the time at which they were enqueued.
|
|
1144
|
+
# If there is a global or local concurrency limit N, select only the N oldest enqueued
|
|
1136
1145
|
# functions, else select all of them.
|
|
1137
1146
|
query = (
|
|
1138
1147
|
sa.select(
|
|
1139
1148
|
SystemSchema.workflow_queue.c.workflow_uuid,
|
|
1140
1149
|
SystemSchema.workflow_queue.c.started_at_epoch_ms,
|
|
1150
|
+
SystemSchema.workflow_queue.c.executor_id,
|
|
1141
1151
|
)
|
|
1142
1152
|
.where(SystemSchema.workflow_queue.c.queue_name == queue.name)
|
|
1143
1153
|
.where(SystemSchema.workflow_queue.c.completed_at_epoch_ms == None)
|
|
1154
|
+
.where(
|
|
1155
|
+
# Only select functions that have not been started yet or have been started by this worker
|
|
1156
|
+
or_(
|
|
1157
|
+
SystemSchema.workflow_queue.c.executor_id == None,
|
|
1158
|
+
SystemSchema.workflow_queue.c.executor_id == executor_id,
|
|
1159
|
+
)
|
|
1160
|
+
)
|
|
1144
1161
|
.order_by(SystemSchema.workflow_queue.c.created_at_epoch_ms.asc())
|
|
1145
1162
|
)
|
|
1146
|
-
|
|
1163
|
+
# Set a dequeue limit if necessary
|
|
1164
|
+
if queue.worker_concurrency is not None:
|
|
1165
|
+
query = query.limit(queue.worker_concurrency)
|
|
1166
|
+
elif queue.concurrency is not None:
|
|
1147
1167
|
query = query.limit(queue.concurrency)
|
|
1148
1168
|
|
|
1149
|
-
# From the functions retrieved, get the workflow IDs of the functions
|
|
1150
|
-
# that have not yet been started so we can start them.
|
|
1151
1169
|
rows = c.execute(query).fetchall()
|
|
1170
|
+
|
|
1171
|
+
# Now, get the workflow IDs of functions that have not yet been started
|
|
1152
1172
|
dequeued_ids: List[str] = [row[0] for row in rows if row[1] is None]
|
|
1153
1173
|
ret_ids: list[str] = []
|
|
1174
|
+
dbos_logger.debug(f"[{queue.name}] dequeueing {len(dequeued_ids)} task(s)")
|
|
1154
1175
|
for id in dequeued_ids:
|
|
1155
1176
|
|
|
1156
1177
|
# If we have a limiter, stop starting functions when the number
|
|
@@ -1173,11 +1194,11 @@ class SystemDatabase:
|
|
|
1173
1194
|
)
|
|
1174
1195
|
)
|
|
1175
1196
|
|
|
1176
|
-
# Then give it a start time
|
|
1197
|
+
# Then give it a start time and assign the executor ID
|
|
1177
1198
|
c.execute(
|
|
1178
1199
|
SystemSchema.workflow_queue.update()
|
|
1179
1200
|
.where(SystemSchema.workflow_queue.c.workflow_uuid == id)
|
|
1180
|
-
.values(started_at_epoch_ms=start_time_ms)
|
|
1201
|
+
.values(started_at_epoch_ms=start_time_ms, executor_id=executor_id)
|
|
1181
1202
|
)
|
|
1182
1203
|
ret_ids.append(id)
|
|
1183
1204
|
|
dbos/dbos-config.schema.json
CHANGED
|
@@ -81,13 +81,7 @@
|
|
|
81
81
|
"type": "array",
|
|
82
82
|
"description": "Specify a list of user DB rollback commands to run"
|
|
83
83
|
}
|
|
84
|
-
}
|
|
85
|
-
"required": [
|
|
86
|
-
"hostname",
|
|
87
|
-
"port",
|
|
88
|
-
"username",
|
|
89
|
-
"password"
|
|
90
|
-
]
|
|
84
|
+
}
|
|
91
85
|
},
|
|
92
86
|
"telemetry": {
|
|
93
87
|
"type": "object",
|
|
@@ -181,9 +175,6 @@
|
|
|
181
175
|
"type": "string",
|
|
182
176
|
"deprecated": true
|
|
183
177
|
}
|
|
184
|
-
}
|
|
185
|
-
"required": [
|
|
186
|
-
"database"
|
|
187
|
-
]
|
|
178
|
+
}
|
|
188
179
|
}
|
|
189
180
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
dbos-0.
|
|
2
|
-
dbos-0.
|
|
3
|
-
dbos-0.
|
|
4
|
-
dbos-0.
|
|
1
|
+
dbos-0.19.0a4.dist-info/METADATA,sha256=NOwdv7iSopa_WvJkvQ3-AJ-peRmVMFW3E5D7SfxERqI,5144
|
|
2
|
+
dbos-0.19.0a4.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
|
|
3
|
+
dbos-0.19.0a4.dist-info/entry_points.txt,sha256=z6GcVANQV7Uw_82H9Ob2axJX6V3imftyZsljdh-M1HU,54
|
|
4
|
+
dbos-0.19.0a4.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
|
|
5
5
|
dbos/__init__.py,sha256=CxRHBHEthPL4PZoLbZhp3rdm44-KkRTT2-7DkK9d4QQ,724
|
|
6
6
|
dbos/_admin_server.py,sha256=DOgzVp9kmwiebQqmJB1LcrZnGTxSMbZiGXdenc1wZDg,3163
|
|
7
7
|
dbos/_app_db.py,sha256=_tv2vmPjjiaikwgxH3mqxgJ4nUUcG2-0uMXKWCqVu1c,5509
|
|
@@ -10,11 +10,11 @@ dbos/_cloudutils/authentication.py,sha256=V0fCWQN9stCkhbuuxgPTGpvuQcDqfU3KAxPAh0
|
|
|
10
10
|
dbos/_cloudutils/cloudutils.py,sha256=5e3CW1deSW-dI5G3QN0XbiVsBhyqT8wu7fuV2f8wtGU,7688
|
|
11
11
|
dbos/_cloudutils/databases.py,sha256=x4187Djsyoa-QaG3Kog8JT2_GERsnqa93LIVanmVUmg,8393
|
|
12
12
|
dbos/_context.py,sha256=KV3fd3-Rv6EWrYDUdHARxltSlNZGNtQtNSqeQ-gkXE8,18049
|
|
13
|
-
dbos/_core.py,sha256=
|
|
13
|
+
dbos/_core.py,sha256=dbG8573iSzB_WITWOh6yOV-w32BM8UbJcOB4Fr0e-lw,34456
|
|
14
14
|
dbos/_croniter.py,sha256=hbhgfsHBqclUS8VeLnJ9PSE9Z54z6mi4nnrr1aUXn0k,47561
|
|
15
|
-
dbos/_db_wizard.py,sha256=
|
|
16
|
-
dbos/_dbos.py,sha256=
|
|
17
|
-
dbos/_dbos_config.py,sha256=
|
|
15
|
+
dbos/_db_wizard.py,sha256=xgKLna0_6Xi50F3o8msRosXba8NScHlpJR5ICVCkHDQ,7534
|
|
16
|
+
dbos/_dbos.py,sha256=LWFa48CPt7bsNAnMZrNDzHHTFCyMrY-nKbMZwCG_dqY,34710
|
|
17
|
+
dbos/_dbos_config.py,sha256=h_q1gzudhsAMVkGMD0qQ6kLic6YhdJgzm50YFSIx9Bo,8196
|
|
18
18
|
dbos/_error.py,sha256=UETk8CoZL-TO2Utn1-E7OSWelhShWmKM-fOlODMR9PE,3893
|
|
19
19
|
dbos/_fastapi.py,sha256=iyefCZq-ZDKRUjN_rgYQmFmyvWf4gPrSlC6CLbfq4a8,3419
|
|
20
20
|
dbos/_flask.py,sha256=z1cijbTi5Dpq6kqikPCx1LcR2YHHv2oc41NehOWjw74,2431
|
|
@@ -23,6 +23,7 @@ dbos/_kafka_message.py,sha256=NYvOXNG3Qn7bghn1pv3fg4Pbs86ILZGcK4IB-MLUNu0,409
|
|
|
23
23
|
dbos/_logger.py,sha256=iYwbA7DLyXalWa2Yu07HO6Xm301nRuenMU64GgwUMkU,3576
|
|
24
24
|
dbos/_migrations/env.py,sha256=38SIGVbmn_VV2x2u1aHLcPOoWgZ84eCymf3g_NljmbU,1626
|
|
25
25
|
dbos/_migrations/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
|
|
26
|
+
dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py,sha256=ICLPl8CN9tQXMsLDsAj8z1TsL831-Z3F8jSBvrR-wyw,736
|
|
26
27
|
dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py,sha256=ZBYrtTdxy64HxIAlOes89fVIk2P1gNaJack7wuC_epg,873
|
|
27
28
|
dbos/_migrations/versions/5c361fc04708_added_system_tables.py,sha256=QMgFMb0aLgC25YicsvPSr6AHRCA6Zd66hyaRUhwKzrQ,6404
|
|
28
29
|
dbos/_migrations/versions/a3b18ad34abe_added_triggers.py,sha256=Rv0ZsZYZ_WdgGEULYsPfnp4YzaO5L198gDTgYY39AVA,2022
|
|
@@ -30,7 +31,7 @@ dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py,sha256=8PyFi8rd6CN-m
|
|
|
30
31
|
dbos/_migrations/versions/d76646551a6c_workflow_queue.py,sha256=G942nophZ2uC2vc4hGBC02Ptng1715roTjY3xiyzZU4,729
|
|
31
32
|
dbos/_migrations/versions/eab0cc1d9a14_job_queue.py,sha256=uvhFOtqbBreCePhAxZfIT0qCAI7BiZTou9wt6QnbY7c,1412
|
|
32
33
|
dbos/_outcome.py,sha256=FDMgWVjZ06vm9xO-38H17mTqBImUYQxgKs_bDCSIAhE,6648
|
|
33
|
-
dbos/_queue.py,sha256=
|
|
34
|
+
dbos/_queue.py,sha256=VSaF-BTv2tm-44O_690omo0pE31NQAhOT3ARL4VLRzY,2723
|
|
34
35
|
dbos/_recovery.py,sha256=jbzGYxICA2drzyzlBSy2UiXhKV_16tBVacKQdTkqf-w,2008
|
|
35
36
|
dbos/_registrations.py,sha256=mei6q6_3R5uei8i_Wo_TqGZs85s10shOekDX41sFYD0,6642
|
|
36
37
|
dbos/_request.py,sha256=cX1B3Atlh160phgS35gF1VEEV4pD126c9F3BDgBmxZU,929
|
|
@@ -38,22 +39,22 @@ dbos/_roles.py,sha256=iOsgmIAf1XVzxs3gYWdGRe1B880YfOw5fpU7Jwx8_A8,2271
|
|
|
38
39
|
dbos/_scheduler.py,sha256=0I3e8Y-OIBG3wiUCIskShd-Sk_eUFCFyRB5u4L7IHXI,1940
|
|
39
40
|
dbos/_schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
41
|
dbos/_schemas/application_database.py,sha256=KeyoPrF7hy_ODXV7QNike_VFSD74QBRfQ76D7QyE9HI,966
|
|
41
|
-
dbos/_schemas/system_database.py,sha256=
|
|
42
|
+
dbos/_schemas/system_database.py,sha256=rwp4EvCSaXcUoMaRczZCvETCxGp72k3-hvLyGUDkih0,5163
|
|
42
43
|
dbos/_serialization.py,sha256=YCYv0qKAwAZ1djZisBC7khvKqG-5OcIv9t9EC5PFIog,1743
|
|
43
|
-
dbos/_sys_db.py,sha256=
|
|
44
|
+
dbos/_sys_db.py,sha256=2W3ta0Q-isESMjyGbXCPfaoll-vyPQg1innBEeNfg2c,50088
|
|
44
45
|
dbos/_templates/hello/README.md,sha256=GhxhBj42wjTt1fWEtwNriHbJuKb66Vzu89G4pxNHw2g,930
|
|
45
46
|
dbos/_templates/hello/__package/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
47
|
dbos/_templates/hello/__package/main.py,sha256=eI0SS9Nwj-fldtiuSzIlIG6dC91GXXwdRsoHxv6S_WI,2719
|
|
47
48
|
dbos/_templates/hello/__package/schema.py,sha256=7Z27JGC8yy7Z44cbVXIREYxtUhU4JVkLCp5Q7UahVQ0,260
|
|
48
49
|
dbos/_templates/hello/alembic.ini,sha256=VKBn4Gy8mMuCdY7Hip1jmo3wEUJ1VG1aW7EqY0_n-as,3695
|
|
49
|
-
dbos/_templates/hello/dbos-config.yaml.dbos,sha256=
|
|
50
|
+
dbos/_templates/hello/dbos-config.yaml.dbos,sha256=OMlcpdYUJKjyAme7phOz3pbn9upcIRjm42iwEThWUEQ,495
|
|
50
51
|
dbos/_templates/hello/migrations/env.py.dbos,sha256=GUV6sjkDzf9Vl6wkGEd0RSkK-ftRfV6EUwSQdd0qFXg,2392
|
|
51
52
|
dbos/_templates/hello/migrations/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
|
|
52
53
|
dbos/_templates/hello/migrations/versions/2024_07_31_180642_init.py,sha256=U5thFWGqNN4QLrNXT7wUUqftIFDNE5eSdqD8JNW1mec,942
|
|
53
54
|
dbos/_templates/hello/start_postgres_docker.py,sha256=lQVLlYO5YkhGPEgPqwGc7Y8uDKse9HsWv5fynJEFJHM,1681
|
|
54
55
|
dbos/_tracer.py,sha256=rvBY1RQU6DO7rL7EnaJJxGcmd4tP_PpGqUEE6imZnhY,2518
|
|
55
56
|
dbos/cli.py,sha256=em1uAxrp5yyg53V7ZpmHFtqD6OJp2cMJkG9vGJPoFTA,10904
|
|
56
|
-
dbos/dbos-config.schema.json,sha256=
|
|
57
|
+
dbos/dbos-config.schema.json,sha256=X5TpXNcARGceX0zQs0fVgtZW_Xj9uBbY5afPt9Rz9yk,5741
|
|
57
58
|
dbos/py.typed,sha256=QfzXT1Ktfk3Rj84akygc7_42z0lRpCq0Ilh8OXI6Zas,44
|
|
58
59
|
version/__init__.py,sha256=L4sNxecRuqdtSFdpUGX3TtBi9KL3k7YsZVIvv-fv9-A,1678
|
|
59
|
-
dbos-0.
|
|
60
|
+
dbos-0.19.0a4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|