Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion backend/druks/api/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
create_async_engine_from_url,
session_scope,
)
from druks.durable.engine import init_dbos, launch, shutdown
from druks.durable.engine import configure_engine, init_dbos, launch, shutdown
from druks.durable.exceptions import AgentCallNotFound
from druks.events.routes import router as events_router
from druks.exceptions import ObjectNotFound
Expand Down Expand Up @@ -67,6 +67,8 @@ def configure_state(app: FastAPI, settings: Settings) -> None:
# Bind the ambient (``scoped_session``) factory to this engine so
# request handlers can use ``db_session()`` without per-call setup.
configure_session(app.state.engine)
# Steps run on the serving loop, so they share the request pool.
configure_engine(app.state.engine)


@asynccontextmanager
Expand Down
8 changes: 4 additions & 4 deletions backend/druks/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,14 @@ def create_async_engine_from_url(database_url: str):
# it. The pool serves every concurrent run's steps plus request handling
# at once: a modest steady pool, with overflow doing the burst work —
# overflow connections open on demand and close on return, so the ceiling
# is high while idle cost is not. Ceiling 50 keeps the appliance (with
# DBOS's two engines at 20 each) inside Postgres's default 100 connections.
# is high while idle cost is not.
settings = load_settings()
return create_async_engine(
database_url,
pool_pre_ping=True,
pool_timeout=5,
pool_size=20,
max_overflow=30,
pool_size=settings.database_pool_size,
max_overflow=settings.database_max_overflow,
)


Expand Down
1 change: 1 addition & 0 deletions backend/druks/durable/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def init_dbos() -> None:
config: DBOSConfig = {
"name": "druks",
"system_database_url": url,
"sys_db_pool_size": settings.dbos_pool_size,
"dbos_system_schema": DBOS_SYSTEM_SCHEMA,
"log_level": settings.log_level,
# One constant application version. DBOS recovers only the runs whose version
Expand Down
4 changes: 4 additions & 0 deletions backend/druks/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ class Settings(BaseSettings):
default="postgresql+psycopg://druks:druks@localhost:5432/druks",
alias="DRUKS_DATABASE_URL",
)
# SQLAlchemy reads a pool size of 0 as unbounded.
database_pool_size: int = Field(default=20, gt=0, alias="DRUKS_DATABASE_POOL_SIZE")
database_max_overflow: int = Field(default=30, ge=0, alias="DRUKS_DATABASE_MAX_OVERFLOW")
dbos_pool_size: int = Field(default=20, gt=0, alias="DRUKS_DBOS_POOL_SIZE")

# Transport only — GitHub credentials live on the service-identity row,
# not in Settings; this points every client at a compatible API endpoint.
Expand Down
19 changes: 19 additions & 0 deletions backend/tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from pathlib import Path

import pytest
from druks import database
from druks.durable import engine as durable_engine
from druks.settings import Settings, ensure_data_dirs
from druks.testing import make_settings
from pydantic import ValidationError
Expand Down Expand Up @@ -184,3 +186,20 @@ def test_development_example_pins_the_installation_timezone(tmp_path, monkeypatc
monkeypatch.setenv("DRUKS_CONFIG", str(config))
monkeypatch.setenv("TIMEZONE", "Asia/Tokyo")
assert Settings(secrets={"secrets_key": _SECRETS_KEY}).timezone == "UTC"


def test_pool_settings_reach_the_app_engine_and_dbos(tmp_path, monkeypatch):
settings = make_settings(
tmp_path, database_pool_size=3, database_max_overflow=4, dbos_pool_size=5
)
monkeypatch.setattr(database, "load_settings", lambda: settings)
monkeypatch.setattr(durable_engine, "load_settings", lambda: settings)
monkeypatch.setattr(durable_engine, "_initialized", False)
configs = []
monkeypatch.setattr(durable_engine, "DBOS", lambda config: configs.append(config))

pool = database.create_async_engine_from_url(settings.database_url).pool
durable_engine.init_dbos()

assert (pool.size(), pool._max_overflow) == (3, 4)
assert configs[0]["sys_db_pool_size"] == 5
4 changes: 2 additions & 2 deletions deploy/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ services:
"--host",
"${DRUKS_WEB_BIND_HOST:-127.0.0.1}",
"--port",
"8001",
"${DRUKS_WEB_PORT:-8001}",
]

drukbox:
Expand Down Expand Up @@ -206,7 +206,7 @@ services:
network_mode: host
environment:
POSTGRES_USER: ${DRUKS_POSTGRES_USER:-druks}
POSTGRES_PASSWORD: ${DRUKS_POSTGRES_PASSWORD:?set DRUKS_POSTGRES_PASSWORD in .env — druks setup generates one}
POSTGRES_PASSWORD: ${DRUKS_POSTGRES_PASSWORD}
POSTGRES_DB: ${DRUKS_POSTGRES_DB:-druks}
volumes:
- postgres_data:/var/lib/postgresql/data
Expand Down
12 changes: 10 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,21 @@ rejects execution settings. The installation API rejects timezone changes.
| Variable | Default | Purpose |
| --- | --- | --- |
| `DRUKS_DATABASE_URL` | local `druks` Postgres | Runtime and DBOS database |
| `DRUKS_DATABASE_POOL_SIZE` | `20` | Connections each process keeps open for requests and workflow steps |
| `DRUKS_DATABASE_MAX_OVERFLOW` | `30` | Extra connections each process opens under load and closes after use |
| `DRUKS_DBOS_POOL_SIZE` | `20` | Connections each process keeps for DBOS |
| `DRUKS_TEST_DATABASE_URL` | local `druks_test` Postgres | What the shipped pytest fixtures use — never the runtime's |
| `DRUKS_TEST_REDIS_URL` | `redis://127.0.0.1:6379/15` | What the shipped pytest fixtures flush |
| `DRUKS_REDIS_URL` | `redis://127.0.0.1:6379/0` | Short-lived coordination and caches |
| `DRUKS_DATA_DIR` | `/var/lib/druks` | Logs, artifacts, installed skills |
| `DRUKS_HARNESS_CONFIG_ROOT` | `~/.config/druks/harnesses` | Optional harness configuration copied into sandboxes |
| `DRUKS_LOG_LEVEL` | `INFO` | Python and DBOS log level |

Each Druks process can open up to `DRUKS_DATABASE_POOL_SIZE` +
`DRUKS_DATABASE_MAX_OVERFLOW` + `DRUKS_DBOS_POOL_SIZE` + 1 Postgres connections.
The extra connection is the DBOS notification listener. With the defaults, this
is 71 of the 100 connections that Postgres allows by default.

Postgres stores durable state. Redis does not store workflow state. It supports
short-lived concerns including webhook delivery claims, OAuth state and token
caches, and the sandbox provisioning gate.
Expand Down Expand Up @@ -181,8 +189,8 @@ order:
can access Druks. Account values are case-insensitive.
3. **JWT mode (`jwt`).** This mode uses the assertion channel from `header` mode, but
its value is a signed JWT. Druks validates the RS256 signature against
`identity.jwks_url`. It caches keys for five minutes and gets new keys after
rotation.
`identity.jwks_url`. It caches keys for one hour. A token signed with an
unknown key ID makes Druks fetch the keys again at once.

The `exp`, `iss`, and `aud` claims must match the configuration.
Druks resolves `identity.jwt_identity_claim` in the verified payload and maps
Expand Down
Loading