fronta 0.1.0__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.
- fronta-0.1.0/LICENSE +21 -0
- fronta-0.1.0/PKG-INFO +226 -0
- fronta-0.1.0/README.md +186 -0
- fronta-0.1.0/THIRD_PARTY_NOTICES.md +30 -0
- fronta-0.1.0/pyproject.toml +123 -0
- fronta-0.1.0/src/fronta/__init__.py +58 -0
- fronta-0.1.0/src/fronta/cli.py +134 -0
- fronta-0.1.0/src/fronta/codec.py +111 -0
- fronta-0.1.0/src/fronta/config.py +62 -0
- fronta-0.1.0/src/fronta/definitions.py +229 -0
- fronta-0.1.0/src/fronta/errors.py +53 -0
- fronta-0.1.0/src/fronta/executors.py +174 -0
- fronta-0.1.0/src/fronta/model.py +289 -0
- fronta-0.1.0/src/fronta/py.typed +0 -0
- fronta-0.1.0/src/fronta/runtime.py +105 -0
- fronta-0.1.0/src/fronta/sandbox.py +564 -0
- fronta-0.1.0/src/fronta/schema.sql +53 -0
- fronta-0.1.0/src/fronta/server/__init__.py +5 -0
- fronta-0.1.0/src/fronta/server/api.py +104 -0
- fronta-0.1.0/src/fronta/server/app.py +189 -0
- fronta-0.1.0/src/fronta/server/mcp.py +107 -0
- fronta-0.1.0/src/fronta/server/service.py +170 -0
- fronta-0.1.0/src/fronta/server/static/alpine.LICENSE.md +21 -0
- fronta-0.1.0/src/fronta/server/static/alpine.min.js +5 -0
- fronta-0.1.0/src/fronta/server/templates/index.html +236 -0
- fronta-0.1.0/src/fronta/store.py +584 -0
- fronta-0.1.0/src/fronta/worker.py +756 -0
fronta-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ondrej
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
fronta-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fronta
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Distributed task queue for asyncio tasks and sandboxed processes
|
|
5
|
+
Keywords: task queue,postgresql,asyncio,sandbox,bubblewrap,mcp,workers
|
|
6
|
+
Author: Ondrej
|
|
7
|
+
Author-email: Ondrej <ondrej@ilcik.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
License-File: THIRD_PARTY_NOTICES.md
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Framework :: Pydantic :: 2
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Dist: click>=8.1.1,<9
|
|
24
|
+
Requires-Dist: psycopg-pool>=3.2.8,<4
|
|
25
|
+
Requires-Dist: psycopg[binary,pool]>=3.2.11,<4
|
|
26
|
+
Requires-Dist: pydantic>=2.12,<3
|
|
27
|
+
Requires-Dist: pydantic-settings>=2.5,<3
|
|
28
|
+
Requires-Dist: fastapi>=0.110,<1 ; extra == 'server'
|
|
29
|
+
Requires-Dist: jinja2>=3.1,<4 ; extra == 'server'
|
|
30
|
+
Requires-Dist: jsonschema[format-nongpl]>=4.20,<5 ; extra == 'server'
|
|
31
|
+
Requires-Dist: mcp>=2.1,<3 ; extra == 'server'
|
|
32
|
+
Requires-Dist: uvicorn>=0.31.1,<1 ; extra == 'server'
|
|
33
|
+
Requires-Python: >=3.12
|
|
34
|
+
Project-URL: Homepage, https://github.com/dreo/fronta
|
|
35
|
+
Project-URL: Repository, https://github.com/dreo/fronta
|
|
36
|
+
Project-URL: Issues, https://github.com/dreo/fronta/issues
|
|
37
|
+
Project-URL: Changelog, https://github.com/dreo/fronta/blob/main/CHANGELOG.md
|
|
38
|
+
Provides-Extra: server
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# Fronta
|
|
42
|
+
|
|
43
|
+
Task queue on PostgreSQL for Python. Workers run `async def` handlers in-process and executables
|
|
44
|
+
in bubblewrap sandboxes; an optional server exposes the queue over REST and MCP with a small
|
|
45
|
+
dashboard.
|
|
46
|
+
|
|
47
|
+
Fronta keeps its tables in a `fronta` schema of your own PostgreSQL 16+ database; there is no
|
|
48
|
+
broker. Workers claim rows with `SELECT … FOR UPDATE SKIP LOCKED`, hold leases renewed by
|
|
49
|
+
heartbeats, and record every state change in one fenced transaction, so the tasks of a crashed or
|
|
50
|
+
stalled worker are reaped and retried while attempts remain. Priorities, scheduled runs, dedupe
|
|
51
|
+
keys, retries with jittered backoff and concurrency limits (per task type and per key) are enforced
|
|
52
|
+
in the database; attempt timeouts and cancellation by the worker.
|
|
53
|
+
|
|
54
|
+
**Status:** alpha. The API and the schema can change between minor versions before 1.0 (see
|
|
55
|
+
`CHANGELOG.md`). Linux, Python 3.12–3.14, PostgreSQL 16+.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv add fronta # SDK + worker
|
|
61
|
+
uv add "fronta[server]" # + REST/MCP server and dashboard
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`pip install fronta` works the same. Hosts that run sandboxed processes need `bwrap` (bubblewrap),
|
|
65
|
+
`prlimit` (util-linux) and unprivileged user namespaces; `import fronta` and the SDK work anywhere.
|
|
66
|
+
|
|
67
|
+
## Example
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# app/tasks.py
|
|
71
|
+
import fronta
|
|
72
|
+
from pydantic import BaseModel
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Resize(BaseModel):
|
|
76
|
+
image_id: int
|
|
77
|
+
width: int
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@fronta.task("resize", input=Resize, max_attempts=5, attempt_timeout=120)
|
|
81
|
+
async def resize(ctx: fronta.Context, job: Resize) -> dict[str, int]:
|
|
82
|
+
await ctx.progress({"stage": "download"})
|
|
83
|
+
... # idempotent work that honors CancelledError
|
|
84
|
+
return {"bytes": 12345}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
worker = fronta.Worker([resize])
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
export FRONTA_DSN=postgresql://user:pass@host/db # the role must be able to create the schema
|
|
92
|
+
fronta db init # creates schema `fronta`; safe to repeat
|
|
93
|
+
fronta worker app.tasks:worker # runs until SIGTERM/SIGINT
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# enqueue.py: any process that reaches the database
|
|
98
|
+
import asyncio
|
|
99
|
+
|
|
100
|
+
import fronta
|
|
101
|
+
from app.tasks import Resize, resize
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def main() -> None:
|
|
105
|
+
await fronta.open_pool() # once, at application start
|
|
106
|
+
try:
|
|
107
|
+
task_id = await resize.enqueue(Resize(image_id=7, width=800), priority=5, key="resize-7")
|
|
108
|
+
print(task_id)
|
|
109
|
+
finally:
|
|
110
|
+
await fronta.close_pool() # at application shutdown
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
asyncio.run(main())
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`enqueue(..., conn=conn)` joins your own psycopg transaction instead of using the pool. `key`
|
|
117
|
+
dedupes: while a task with the same key is queued or running, `enqueue` returns its id; once
|
|
118
|
+
that task has finished, the same key enqueues a new one.
|
|
119
|
+
|
|
120
|
+
A handler gets the validated input and a `Context` (`task_id`, `attempt`, `log`, `progress()`,
|
|
121
|
+
`enqueue()`, `cancelled`, `state` from the worker lifespan). It must handle
|
|
122
|
+
`asyncio.CancelledError` and be safe to run twice: after a lost lease the task runs again.
|
|
123
|
+
|
|
124
|
+
A sandboxed process task, with a placeholder executable:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
class Convert(BaseModel):
|
|
128
|
+
source: str
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
convert = fronta.process_task(
|
|
132
|
+
"convert",
|
|
133
|
+
["/usr/bin/convert-tool", "--from-stdin"], # reads the JSON input on stdin
|
|
134
|
+
input=Convert,
|
|
135
|
+
sandbox=fronta.Sandbox(memory_bytes=512 << 20, cpu_time_s=60, max_pids=16),
|
|
136
|
+
max_concurrency=4,
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The process runs in a private tmpfs `/work` without network; its result is
|
|
141
|
+
`{"exit_code", "stdout", "stderr", "truncated"}`. Exit code 0 means the task succeeded; anything
|
|
142
|
+
else fails the attempt.
|
|
143
|
+
|
|
144
|
+
## Server
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
FRONTA_SERVER_TOKEN=... fronta server # 127.0.0.1:8000
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
REST under `/api/v1` (task types, enqueue, get, list, cancel), MCP at `/mcp`, dashboard at `/`.
|
|
151
|
+
The SDK only enqueues; inspection and cancellation go through the server. `FRONTA_SERVER_TOKEN`
|
|
152
|
+
is required (the server never runs open: even on loopback a browser could be made to cancel or
|
|
153
|
+
enqueue tasks); every REST and MCP request sends it as `Authorization: Bearer <token>`. Put a
|
|
154
|
+
TLS-terminating reverse proxy in front of it outside a private network. Endpoints, inputs and error codes:
|
|
155
|
+
[docs/reference.md](https://github.com/dreo/fronta/blob/main/docs/reference.md#server).
|
|
156
|
+
|
|
157
|
+
## Deploy
|
|
158
|
+
|
|
159
|
+
One database, any number of workers, optionally a server. Each process reads `FRONTA_*`
|
|
160
|
+
environment variables; `FRONTA_DSN` is the only required one. Run workers under a supervisor that
|
|
161
|
+
restarts them: a worker exits 0 after a graceful stop and 70 when a handler ignores cancellation or
|
|
162
|
+
blocks the event loop.
|
|
163
|
+
|
|
164
|
+
```ini
|
|
165
|
+
# /etc/systemd/system/fronta-worker.service
|
|
166
|
+
[Unit]
|
|
167
|
+
Description=Fronta worker
|
|
168
|
+
After=network-online.target
|
|
169
|
+
|
|
170
|
+
[Service]
|
|
171
|
+
User=app
|
|
172
|
+
WorkingDirectory=/srv/app
|
|
173
|
+
# FRONTA_DSN=... and other FRONTA_* variables; readable by root only (mode 0600)
|
|
174
|
+
EnvironmentFile=/etc/fronta/worker.env
|
|
175
|
+
ExecStart=/srv/app/.venv/bin/fronta worker app.tasks:worker
|
|
176
|
+
# SIGTERM goes to the worker only, which stops its sandboxes itself; SIGKILL to everything
|
|
177
|
+
KillMode=mixed
|
|
178
|
+
Restart=always
|
|
179
|
+
RestartSec=2
|
|
180
|
+
TimeoutStopSec=120
|
|
181
|
+
|
|
182
|
+
[Install]
|
|
183
|
+
WantedBy=multi-user.target
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`systemctl enable --now fronta-worker`. On `SIGTERM` the worker stops claiming, lets running
|
|
187
|
+
attempts finish for `FRONTA_GRACE_S`, then stops the rest (another grace period for cooperative
|
|
188
|
+
cancellation, then a kill) and records every outcome before it exits. That takes at most
|
|
189
|
+
`2 × FRONTA_GRACE_S + 6 × FRONTA_KILL_TIMEOUT_S` (91 s with the defaults 30 s and 5 s) unless the
|
|
190
|
+
database is unreachable or a sandbox cannot be killed: then the worker keeps trying rather than
|
|
191
|
+
lose an outcome, and systemd's `SIGKILL` at `TimeoutStopSec` ends it. That loses no data:
|
|
192
|
+
sandboxes die with the worker and unrecorded attempts are retried when their lease expires.
|
|
193
|
+
|
|
194
|
+
Throughput is bounded by the database's commit rate (at least two durable commits per task, plus
|
|
195
|
+
heartbeats and progress): about 150 no-op tasks/s in total on a laptop PostgreSQL with fsync; a
|
|
196
|
+
claim costs ~5 ms on a 70k-row queue. Configuration, retry policy, guarantees and the
|
|
197
|
+
measurements: [docs/reference.md](https://github.com/dreo/fronta/blob/main/docs/reference.md).
|
|
198
|
+
|
|
199
|
+
## Not covered
|
|
200
|
+
|
|
201
|
+
- Exactly-once side effects: a worker stalled past its lease may still be running while the task
|
|
202
|
+
is retried elsewhere; the stale attempt's writes to Fronta are rejected, its other effects are not.
|
|
203
|
+
- Workflows, chains, or periodic tasks (only `run_at`).
|
|
204
|
+
- Schema migrations before 1.0: a release that changes the schema needs `fronta db init` on a
|
|
205
|
+
fresh schema.
|
|
206
|
+
- Windows or macOS workers: sandboxes are Linux-only and nothing else is tested there.
|
|
207
|
+
|
|
208
|
+
## Development
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
uv sync --all-extras
|
|
212
|
+
docker run -d --name fronta-test-pg -e POSTGRES_USER=fronta -e POSTGRES_PASSWORD=fronta \
|
|
213
|
+
-e POSTGRES_DB=fronta -p 127.0.0.1:5439:5432 postgres:16
|
|
214
|
+
export FRONTA_TEST_DSN=postgresql://fronta:fronta@127.0.0.1:5439/fronta
|
|
215
|
+
make check # lint, format, types, architecture, deps (also the git pre-commit hook)
|
|
216
|
+
make checkall # check + the full test suite + pip-audit
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
CI runs `make checkall` on every pull request and push to `main`, on Python 3.12–3.14 and at the
|
|
220
|
+
lowest dependency versions the declared bounds allow. To release: set the version (`uv version X.Y.Z`), add the CHANGELOG
|
|
221
|
+
section, merge, then push the tag `vX.Y.Z` from that `main` commit; the gate runs again, the
|
|
222
|
+
package goes to PyPI and a GitHub release is created. `SPEC.md` is the contract.
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
MIT. The dashboard bundles Alpine.js (MIT); see `THIRD_PARTY_NOTICES.md`.
|
fronta-0.1.0/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Fronta
|
|
2
|
+
|
|
3
|
+
Task queue on PostgreSQL for Python. Workers run `async def` handlers in-process and executables
|
|
4
|
+
in bubblewrap sandboxes; an optional server exposes the queue over REST and MCP with a small
|
|
5
|
+
dashboard.
|
|
6
|
+
|
|
7
|
+
Fronta keeps its tables in a `fronta` schema of your own PostgreSQL 16+ database; there is no
|
|
8
|
+
broker. Workers claim rows with `SELECT … FOR UPDATE SKIP LOCKED`, hold leases renewed by
|
|
9
|
+
heartbeats, and record every state change in one fenced transaction, so the tasks of a crashed or
|
|
10
|
+
stalled worker are reaped and retried while attempts remain. Priorities, scheduled runs, dedupe
|
|
11
|
+
keys, retries with jittered backoff and concurrency limits (per task type and per key) are enforced
|
|
12
|
+
in the database; attempt timeouts and cancellation by the worker.
|
|
13
|
+
|
|
14
|
+
**Status:** alpha. The API and the schema can change between minor versions before 1.0 (see
|
|
15
|
+
`CHANGELOG.md`). Linux, Python 3.12–3.14, PostgreSQL 16+.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv add fronta # SDK + worker
|
|
21
|
+
uv add "fronta[server]" # + REST/MCP server and dashboard
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`pip install fronta` works the same. Hosts that run sandboxed processes need `bwrap` (bubblewrap),
|
|
25
|
+
`prlimit` (util-linux) and unprivileged user namespaces; `import fronta` and the SDK work anywhere.
|
|
26
|
+
|
|
27
|
+
## Example
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
# app/tasks.py
|
|
31
|
+
import fronta
|
|
32
|
+
from pydantic import BaseModel
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Resize(BaseModel):
|
|
36
|
+
image_id: int
|
|
37
|
+
width: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@fronta.task("resize", input=Resize, max_attempts=5, attempt_timeout=120)
|
|
41
|
+
async def resize(ctx: fronta.Context, job: Resize) -> dict[str, int]:
|
|
42
|
+
await ctx.progress({"stage": "download"})
|
|
43
|
+
... # idempotent work that honors CancelledError
|
|
44
|
+
return {"bytes": 12345}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
worker = fronta.Worker([resize])
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
export FRONTA_DSN=postgresql://user:pass@host/db # the role must be able to create the schema
|
|
52
|
+
fronta db init # creates schema `fronta`; safe to repeat
|
|
53
|
+
fronta worker app.tasks:worker # runs until SIGTERM/SIGINT
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# enqueue.py: any process that reaches the database
|
|
58
|
+
import asyncio
|
|
59
|
+
|
|
60
|
+
import fronta
|
|
61
|
+
from app.tasks import Resize, resize
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def main() -> None:
|
|
65
|
+
await fronta.open_pool() # once, at application start
|
|
66
|
+
try:
|
|
67
|
+
task_id = await resize.enqueue(Resize(image_id=7, width=800), priority=5, key="resize-7")
|
|
68
|
+
print(task_id)
|
|
69
|
+
finally:
|
|
70
|
+
await fronta.close_pool() # at application shutdown
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
asyncio.run(main())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`enqueue(..., conn=conn)` joins your own psycopg transaction instead of using the pool. `key`
|
|
77
|
+
dedupes: while a task with the same key is queued or running, `enqueue` returns its id; once
|
|
78
|
+
that task has finished, the same key enqueues a new one.
|
|
79
|
+
|
|
80
|
+
A handler gets the validated input and a `Context` (`task_id`, `attempt`, `log`, `progress()`,
|
|
81
|
+
`enqueue()`, `cancelled`, `state` from the worker lifespan). It must handle
|
|
82
|
+
`asyncio.CancelledError` and be safe to run twice: after a lost lease the task runs again.
|
|
83
|
+
|
|
84
|
+
A sandboxed process task, with a placeholder executable:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
class Convert(BaseModel):
|
|
88
|
+
source: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
convert = fronta.process_task(
|
|
92
|
+
"convert",
|
|
93
|
+
["/usr/bin/convert-tool", "--from-stdin"], # reads the JSON input on stdin
|
|
94
|
+
input=Convert,
|
|
95
|
+
sandbox=fronta.Sandbox(memory_bytes=512 << 20, cpu_time_s=60, max_pids=16),
|
|
96
|
+
max_concurrency=4,
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The process runs in a private tmpfs `/work` without network; its result is
|
|
101
|
+
`{"exit_code", "stdout", "stderr", "truncated"}`. Exit code 0 means the task succeeded; anything
|
|
102
|
+
else fails the attempt.
|
|
103
|
+
|
|
104
|
+
## Server
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
FRONTA_SERVER_TOKEN=... fronta server # 127.0.0.1:8000
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
REST under `/api/v1` (task types, enqueue, get, list, cancel), MCP at `/mcp`, dashboard at `/`.
|
|
111
|
+
The SDK only enqueues; inspection and cancellation go through the server. `FRONTA_SERVER_TOKEN`
|
|
112
|
+
is required (the server never runs open: even on loopback a browser could be made to cancel or
|
|
113
|
+
enqueue tasks); every REST and MCP request sends it as `Authorization: Bearer <token>`. Put a
|
|
114
|
+
TLS-terminating reverse proxy in front of it outside a private network. Endpoints, inputs and error codes:
|
|
115
|
+
[docs/reference.md](https://github.com/dreo/fronta/blob/main/docs/reference.md#server).
|
|
116
|
+
|
|
117
|
+
## Deploy
|
|
118
|
+
|
|
119
|
+
One database, any number of workers, optionally a server. Each process reads `FRONTA_*`
|
|
120
|
+
environment variables; `FRONTA_DSN` is the only required one. Run workers under a supervisor that
|
|
121
|
+
restarts them: a worker exits 0 after a graceful stop and 70 when a handler ignores cancellation or
|
|
122
|
+
blocks the event loop.
|
|
123
|
+
|
|
124
|
+
```ini
|
|
125
|
+
# /etc/systemd/system/fronta-worker.service
|
|
126
|
+
[Unit]
|
|
127
|
+
Description=Fronta worker
|
|
128
|
+
After=network-online.target
|
|
129
|
+
|
|
130
|
+
[Service]
|
|
131
|
+
User=app
|
|
132
|
+
WorkingDirectory=/srv/app
|
|
133
|
+
# FRONTA_DSN=... and other FRONTA_* variables; readable by root only (mode 0600)
|
|
134
|
+
EnvironmentFile=/etc/fronta/worker.env
|
|
135
|
+
ExecStart=/srv/app/.venv/bin/fronta worker app.tasks:worker
|
|
136
|
+
# SIGTERM goes to the worker only, which stops its sandboxes itself; SIGKILL to everything
|
|
137
|
+
KillMode=mixed
|
|
138
|
+
Restart=always
|
|
139
|
+
RestartSec=2
|
|
140
|
+
TimeoutStopSec=120
|
|
141
|
+
|
|
142
|
+
[Install]
|
|
143
|
+
WantedBy=multi-user.target
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`systemctl enable --now fronta-worker`. On `SIGTERM` the worker stops claiming, lets running
|
|
147
|
+
attempts finish for `FRONTA_GRACE_S`, then stops the rest (another grace period for cooperative
|
|
148
|
+
cancellation, then a kill) and records every outcome before it exits. That takes at most
|
|
149
|
+
`2 × FRONTA_GRACE_S + 6 × FRONTA_KILL_TIMEOUT_S` (91 s with the defaults 30 s and 5 s) unless the
|
|
150
|
+
database is unreachable or a sandbox cannot be killed: then the worker keeps trying rather than
|
|
151
|
+
lose an outcome, and systemd's `SIGKILL` at `TimeoutStopSec` ends it. That loses no data:
|
|
152
|
+
sandboxes die with the worker and unrecorded attempts are retried when their lease expires.
|
|
153
|
+
|
|
154
|
+
Throughput is bounded by the database's commit rate (at least two durable commits per task, plus
|
|
155
|
+
heartbeats and progress): about 150 no-op tasks/s in total on a laptop PostgreSQL with fsync; a
|
|
156
|
+
claim costs ~5 ms on a 70k-row queue. Configuration, retry policy, guarantees and the
|
|
157
|
+
measurements: [docs/reference.md](https://github.com/dreo/fronta/blob/main/docs/reference.md).
|
|
158
|
+
|
|
159
|
+
## Not covered
|
|
160
|
+
|
|
161
|
+
- Exactly-once side effects: a worker stalled past its lease may still be running while the task
|
|
162
|
+
is retried elsewhere; the stale attempt's writes to Fronta are rejected, its other effects are not.
|
|
163
|
+
- Workflows, chains, or periodic tasks (only `run_at`).
|
|
164
|
+
- Schema migrations before 1.0: a release that changes the schema needs `fronta db init` on a
|
|
165
|
+
fresh schema.
|
|
166
|
+
- Windows or macOS workers: sandboxes are Linux-only and nothing else is tested there.
|
|
167
|
+
|
|
168
|
+
## Development
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
uv sync --all-extras
|
|
172
|
+
docker run -d --name fronta-test-pg -e POSTGRES_USER=fronta -e POSTGRES_PASSWORD=fronta \
|
|
173
|
+
-e POSTGRES_DB=fronta -p 127.0.0.1:5439:5432 postgres:16
|
|
174
|
+
export FRONTA_TEST_DSN=postgresql://fronta:fronta@127.0.0.1:5439/fronta
|
|
175
|
+
make check # lint, format, types, architecture, deps (also the git pre-commit hook)
|
|
176
|
+
make checkall # check + the full test suite + pip-audit
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
CI runs `make checkall` on every pull request and push to `main`, on Python 3.12–3.14 and at the
|
|
180
|
+
lowest dependency versions the declared bounds allow. To release: set the version (`uv version X.Y.Z`), add the CHANGELOG
|
|
181
|
+
section, merge, then push the tag `vX.Y.Z` from that `main` commit; the gate runs again, the
|
|
182
|
+
package goes to PyPI and a GitHub release is created. `SPEC.md` is the contract.
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
MIT. The dashboard bundles Alpine.js (MIT); see `THIRD_PARTY_NOTICES.md`.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
## Alpine.js 3.14.9 (vendored as `fronta/server/static/alpine.min.js`)
|
|
4
|
+
|
|
5
|
+
Source: https://cdn.jsdelivr.net/npm/alpinejs@3.14.9/dist/cdn.min.js
|
|
6
|
+
(https://github.com/alpinejs/alpine), SHA-256 `3ed1eed252488921df65e363d6715deb04d7f92aaedb9e52199fdf73cb1e0ad3`.
|
|
7
|
+
To update: download the new `dist/cdn.min.js`, replace the file, update the version and the
|
|
8
|
+
checksum here, and run the dashboard test (`tests/test_dashboard.py`).
|
|
9
|
+
|
|
10
|
+
MIT License
|
|
11
|
+
|
|
12
|
+
Copyright © 2019-2025 Caleb Porzio and contributors
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fronta"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Distributed task queue for asyncio tasks and sandboxed processes"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Ondrej", email = "ondrej@ilcik.com" }
|
|
8
|
+
]
|
|
9
|
+
license = "MIT"
|
|
10
|
+
license-files = ["LICENSE", "THIRD_PARTY_NOTICES.md"]
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
keywords = ["task queue", "postgresql", "asyncio", "sandbox", "bubblewrap", "mcp", "workers"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Framework :: AsyncIO",
|
|
16
|
+
"Framework :: FastAPI",
|
|
17
|
+
"Framework :: Pydantic :: 2",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: POSIX :: Linux",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Topic :: System :: Distributed Computing",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
# Every floor is tested: CI runs the suite at `--resolution lowest-direct` with all extras and
|
|
29
|
+
# these are the versions it installs. Ceilings exclude the next major of contracts we depend
|
|
30
|
+
# on. `psycopg[binary]` gives an out-of-the-box install; an application that ships
|
|
31
|
+
# `psycopg[c]` keeps it (psycopg prefers the C implementation).
|
|
32
|
+
"click>=8.1.1,<9", # 8.1.0's decorators are untyped for mypy
|
|
33
|
+
"psycopg-pool>=3.2.8,<4", # 3.2.8: CancelledError inside connection() no longer swallowed
|
|
34
|
+
"psycopg[binary,pool]>=3.2.11,<4", # 3.2.4 keeps notifies across notifies() restarts; 3.2.10/11 cancel + wait fixes
|
|
35
|
+
"pydantic>=2.12,<3", # mcp 2.1 needs 2.12; the lowest version CI tests
|
|
36
|
+
"pydantic-settings>=2.5,<3",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/dreo/fronta"
|
|
41
|
+
Repository = "https://github.com/dreo/fronta"
|
|
42
|
+
Issues = "https://github.com/dreo/fronta/issues"
|
|
43
|
+
Changelog = "https://github.com/dreo/fronta/blob/main/CHANGELOG.md"
|
|
44
|
+
|
|
45
|
+
[project.optional-dependencies]
|
|
46
|
+
# `fronta server` (REST + MCP + dashboard); workers and the SDK do not need it.
|
|
47
|
+
server = [
|
|
48
|
+
"fastapi>=0.110,<1", # lifespan= and Annotated dependencies
|
|
49
|
+
"jinja2>=3.1,<4",
|
|
50
|
+
"jsonschema[format-nongpl]>=4.20,<5", # Draft 2020-12 + FORMAT_CHECKER
|
|
51
|
+
"mcp>=2.1,<3", # MCPServer + streamable_http_app(host=, max_request_body_size=)
|
|
52
|
+
"uvicorn>=0.31.1,<1", # timeout_graceful_shutdown=
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[project.scripts]
|
|
56
|
+
fronta = "fronta.cli:main"
|
|
57
|
+
|
|
58
|
+
[build-system]
|
|
59
|
+
requires = ["uv_build>=0.11.18,<0.12.0"]
|
|
60
|
+
build-backend = "uv_build"
|
|
61
|
+
|
|
62
|
+
[dependency-groups]
|
|
63
|
+
dev = [
|
|
64
|
+
"deptry>=0.25.1",
|
|
65
|
+
"httpx>=0.28.1",
|
|
66
|
+
"import-linter>=2.14",
|
|
67
|
+
"mypy>=2.3.1",
|
|
68
|
+
"pip-audit>=2.10.1",
|
|
69
|
+
"playwright>=1.62.0",
|
|
70
|
+
"pre-commit>=4.6.2",
|
|
71
|
+
"pytest>=9.1.1",
|
|
72
|
+
"pytest-asyncio>=1.4.0",
|
|
73
|
+
"ruff>=0.16.5",
|
|
74
|
+
"types-jsonschema>=4.26.0.20260518",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
[tool.ruff]
|
|
78
|
+
target-version = "py312"
|
|
79
|
+
line-length = 100
|
|
80
|
+
|
|
81
|
+
[tool.ruff.lint]
|
|
82
|
+
select = [
|
|
83
|
+
"E", "W", "F", "I", "S", "B", "C4", "UP", "D", "PT",
|
|
84
|
+
"RUF", "ANN", "SIM", "TC", "ARG", "ERA", "PL", "PTH", "DTZ", "T20",
|
|
85
|
+
]
|
|
86
|
+
ignore = ["D1"]
|
|
87
|
+
|
|
88
|
+
[tool.ruff.lint.pydocstyle]
|
|
89
|
+
convention = "google"
|
|
90
|
+
|
|
91
|
+
[tool.ruff.lint.per-file-ignores]
|
|
92
|
+
"tests/**" = ["S101", "D", "ANN", "PLR2004"]
|
|
93
|
+
|
|
94
|
+
[tool.mypy]
|
|
95
|
+
strict = true
|
|
96
|
+
packages = ["fronta"]
|
|
97
|
+
mypy_path = "src"
|
|
98
|
+
|
|
99
|
+
[tool.importlinter]
|
|
100
|
+
root_package = "fronta"
|
|
101
|
+
|
|
102
|
+
[[tool.importlinter.contracts]]
|
|
103
|
+
name = "Layers: cli > server | worker > executors > sandbox | definitions > runtime > store > codec > model | config | errors"
|
|
104
|
+
type = "layers"
|
|
105
|
+
layers = [
|
|
106
|
+
"fronta.cli",
|
|
107
|
+
"fronta.server | fronta.worker",
|
|
108
|
+
"fronta.executors",
|
|
109
|
+
"fronta.sandbox | fronta.definitions",
|
|
110
|
+
"fronta.runtime",
|
|
111
|
+
"fronta.store",
|
|
112
|
+
"fronta.codec",
|
|
113
|
+
"fronta.model | fronta.config | fronta.errors",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
[tool.deptry]
|
|
117
|
+
# Add per-rule ignores only with a justifying comment.
|
|
118
|
+
|
|
119
|
+
[tool.pytest.ini_options]
|
|
120
|
+
asyncio_mode = "auto"
|
|
121
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
122
|
+
asyncio_default_test_loop_scope = "function"
|
|
123
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Fronta: distributed task processing on PostgreSQL with sandboxed process execution."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
from fronta.config import Settings
|
|
6
|
+
from fronta.definitions import Context, ProcessTaskDefinition, TaskDefinition, process_task, task
|
|
7
|
+
from fronta.errors import (
|
|
8
|
+
ConfigurationError,
|
|
9
|
+
FrontaError,
|
|
10
|
+
InputValidationError,
|
|
11
|
+
InvalidInput,
|
|
12
|
+
NonRetryableError,
|
|
13
|
+
NotCancellable,
|
|
14
|
+
PayloadTooLarge,
|
|
15
|
+
ProgressTooLarge,
|
|
16
|
+
ResultSerializationError,
|
|
17
|
+
SandboxError,
|
|
18
|
+
TaskNotFound,
|
|
19
|
+
UnknownTaskType,
|
|
20
|
+
)
|
|
21
|
+
from fronta.model import Backoff, Policy, Sandbox, State, TaskRow, TaskSummary, TaskTypeRow
|
|
22
|
+
from fronta.runtime import close_pool, configure, open_pool
|
|
23
|
+
from fronta.worker import Worker
|
|
24
|
+
|
|
25
|
+
__version__: str = version("fronta")
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Backoff",
|
|
29
|
+
"ConfigurationError",
|
|
30
|
+
"Context",
|
|
31
|
+
"FrontaError",
|
|
32
|
+
"InputValidationError",
|
|
33
|
+
"InvalidInput",
|
|
34
|
+
"NonRetryableError",
|
|
35
|
+
"NotCancellable",
|
|
36
|
+
"PayloadTooLarge",
|
|
37
|
+
"Policy",
|
|
38
|
+
"ProcessTaskDefinition",
|
|
39
|
+
"ProgressTooLarge",
|
|
40
|
+
"ResultSerializationError",
|
|
41
|
+
"Sandbox",
|
|
42
|
+
"SandboxError",
|
|
43
|
+
"Settings",
|
|
44
|
+
"State",
|
|
45
|
+
"TaskDefinition",
|
|
46
|
+
"TaskNotFound",
|
|
47
|
+
"TaskRow",
|
|
48
|
+
"TaskSummary",
|
|
49
|
+
"TaskTypeRow",
|
|
50
|
+
"UnknownTaskType",
|
|
51
|
+
"Worker",
|
|
52
|
+
"__version__",
|
|
53
|
+
"close_pool",
|
|
54
|
+
"configure",
|
|
55
|
+
"open_pool",
|
|
56
|
+
"process_task",
|
|
57
|
+
"task",
|
|
58
|
+
]
|