Skip to content

Commit b428c30

Browse files
Merge pull request #7 from manuschillerdev/docs/readme
Update README for current SDK API
2 parents ff39600 + 74e1c68 commit b428c30

1 file changed

Lines changed: 147 additions & 21 deletions

File tree

README.md

Lines changed: 147 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,47 @@ Community Rust SDK for [Absurd](https://github.com/earendil-works/absurd), a Pos
44

55
This repository is intentionally independent from upstream while the Rust API is tested and hardened.
66

7+
## Status
8+
9+
Experimental community SDK. Implemented today:
10+
11+
- Typed task registration and spawning with `serde` parameters and results.
12+
- One-shot batch execution and long-running Tokio workers.
13+
- Durable task context helpers for checkpointed steps, sleeps, event waits, event emission, and claim heartbeats.
14+
- Retry strategies, cancellation policy wiring, idempotent spawn, queue lifecycle, cleanup, and unknown-task handling.
15+
- Database integration tests for queue lifecycle, typed tasks, checkpointing, sleeps, events, idempotency, retries, unknown tasks, and cancellation.
16+
17+
Still hardening:
18+
19+
- Additional parity coverage against upstream SDK behavior.
20+
- Task-result polling APIs for Absurd SQL versions that expose `get_task_result`.
21+
- Optional spawn/execution middleware for tracing and header propagation.
22+
- Broader built-in TLS and pool configuration. Use `Client::from_pool` for custom pool setup today.
23+
724
## Goals
825

926
- Async-only Tokio SDK.
1027
- Typed task parameters, task results, step results, and event payloads through `serde`.
1128
- No `Box::pin` in user code.
1229
- Keep durable semantics in Absurd's Postgres stored procedures.
13-
- Small API surface: client, task registry, worker, task context, spawn options.
30+
- Small API surface around `Client`, typed `Task`s, `TaskContext`, workers, and options builders.
31+
32+
## Prerequisites
33+
34+
- Rust 1.85+ and edition 2024.
35+
- A Postgres database initialized with Absurd's SQL schema and stored procedures.
36+
- A database connection through `ABSURD_DATABASE_URL`, `DATABASE_URL`, or `PGDATABASE`.
37+
38+
Add the crate and normal async/serialization dependencies:
39+
40+
```toml
41+
[dependencies]
42+
absurd-rust-sdk = "0.1"
43+
serde = { version = "1", features = ["derive"] }
44+
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "time"] }
45+
```
46+
47+
`Client::create_queue()` creates the queue tables inside an initialized Absurd schema; it does not install Absurd itself.
1448

1549
## Quick start
1650

@@ -19,12 +53,12 @@ use absurd_rust_sdk::{Client, Result, Task, WorkBatchOptions};
1953
use serde::{Deserialize, Serialize};
2054
use std::time::Duration;
2155

22-
#[derive(Deserialize, Serialize)]
56+
#[derive(Debug, Deserialize, Serialize)]
2357
struct Params {
2458
name: String,
2559
}
2660

27-
#[derive(Serialize)]
61+
#[derive(Debug, Serialize)]
2862
struct Output {
2963
message: String,
3064
}
@@ -44,39 +78,128 @@ async fn main() -> Result<()> {
4478
Ok(Output { message })
4579
})?;
4680

47-
client
81+
let spawned = client
4882
.spawn(
4983
&hello,
5084
Params { name: "Absurd".into() },
5185
Default::default(),
5286
)
5387
.await?;
5488

89+
println!("spawned task {} run {}", spawned.task_id, spawned.run_id);
90+
5591
client
5692
.work_batch(WorkBatchOptions::new().claim_timeout(Duration::from_secs(120)))
5793
.await?;
94+
5895
Ok(())
5996
}
6097
```
6198

99+
Run the included examples with an initialized Absurd database:
100+
101+
```sh
102+
ABSURD_DATABASE_URL=postgresql://localhost/absurd cargo run --example hello_world
103+
ABSURD_DATABASE_URL=postgresql://localhost/absurd cargo run --example worker
104+
```
105+
106+
## Workers
107+
108+
Use `work_batch` for one-shot processing and `start_worker` for a polling worker:
109+
110+
```rust
111+
use absurd_rust_sdk::{Client, Result, WorkerOptions};
112+
use std::time::Duration;
113+
114+
#[tokio::main]
115+
async fn main() -> Result<()> {
116+
let client = Client::from_env_queue("orders").await?;
117+
118+
// Register tasks before starting the worker.
119+
120+
let worker = client.start_worker(
121+
WorkerOptions::new()
122+
.concurrency(8)
123+
.claim_timeout(Duration::from_secs(120)),
124+
)?;
125+
126+
tokio::signal::ctrl_c()
127+
.await
128+
.map_err(|err| absurd_rust_sdk::Error::message(err.to_string()))?;
129+
130+
worker.close().await?;
131+
Ok(())
132+
}
133+
```
134+
135+
`WorkerOptions` also supports `worker_id`, `batch_size`, `poll_interval`, `unknown_task_policy`, and `fatal_on_lease_timeout`.
136+
62137
## Durable context API
63138

64139
Inside a task handler:
65140

66141
```rust
67-
let value: MyStepResult = ctx.step("step-name", || async {
68-
Ok(do_side_effect_once().await?)
69-
}).await?;
142+
let task_id = ctx.task_id();
143+
let run_id = ctx.run_id();
144+
let attempt = ctx.attempt();
145+
let headers = ctx.headers();
146+
147+
let value: MyStepResult = ctx
148+
.step("step-name", || async { Ok(do_side_effect_once().await?) })
149+
.await?;
70150

71151
ctx.sleep_for("cooldown", Duration::from_secs(60)).await?;
152+
ctx.sleep_until("wake-at", wake_at).await?;
72153

73-
let event: MyEvent = ctx.await_event("payment.confirmed").await?;
154+
let event: MyEvent = ctx
155+
.await_event_with_options(
156+
"payment.confirmed",
157+
AwaitEventOptions::new()
158+
.step_name("wait-for-payment")
159+
.timeout(Duration::from_secs(60 * 60)),
160+
)
161+
.await?;
74162

75163
ctx.emit_event("order.done", &serde_json::json!({ "ok": true })).await?;
76164
ctx.heartbeat().await?;
165+
ctx.heartbeat_for(Duration::from_secs(120)).await?;
77166
```
78167

79-
Each step result is checkpointed in Postgres. If the task retries, completed steps are loaded from the checkpoint table and are not re-executed.
168+
`step`, `sleep_for`/`sleep_until`, and `await_event`/`await_event_with_options` checkpoint their state in Postgres. If a task retries or resumes, completed checkpoints are loaded and not re-executed.
169+
170+
Keep checkpoint names and control flow deterministic. Repeated names in one execution are suffixed automatically (`name`, `name#2`, `name#3`, ...).
171+
172+
## Spawning and options
173+
174+
```rust
175+
use absurd_rust_sdk::{CancellationPolicy, RetryStrategy, SpawnOptions};
176+
use std::time::Duration;
177+
178+
let spawned = client
179+
.spawn(
180+
&task,
181+
params,
182+
SpawnOptions::new()
183+
.idempotency_key("daily-report:2026-06-04")
184+
.max_attempts(3)
185+
.retry_strategy(RetryStrategy::fixed(Duration::from_secs(30)))
186+
.cancellation(CancellationPolicy::new().max_duration(Duration::from_secs(300))),
187+
)
188+
.await?;
189+
190+
if !spawned.created {
191+
println!("idempotency key reused existing task {}", spawned.task_id);
192+
}
193+
```
194+
195+
Useful public types:
196+
197+
- `Client` / `Absurd`: connect, queue lifecycle, registration, spawning, events, cancellation, cleanup, and execution.
198+
- `Task<P, R>` and `TaskOptions`: typed task names, queues, default attempts, and default cancellation.
199+
- `SpawnOptions` and `SpawnResult`: queue override, attempts, retry strategy, headers, cancellation, idempotency, task/run IDs, and creation status.
200+
- `TaskContext` and `AwaitEventOptions`: task metadata, checkpointed steps/sleeps/events, event timeouts, event emission, and heartbeats.
201+
- `WorkBatchOptions`, `WorkerOptions`, and `UnknownTaskPolicy`: execution and worker behavior.
202+
- `CleanupResult`, `Error`, `Result`, and `Json`.
80203

81204
## Database contract
82205

@@ -91,7 +214,8 @@ The SDK calls Absurd stored procedures directly:
91214
- `absurd.fail_run`
92215
- `absurd.schedule_run`
93216
- `absurd.set_task_checkpoint_state`
94-
- `absurd.get_task_checkpoint_state(s)`
217+
- `absurd.get_task_checkpoint_state`
218+
- `absurd.get_task_checkpoint_states`
95219
- `absurd.await_event`
96220
- `absurd.emit_event`
97221
- `absurd.extend_claim`
@@ -102,23 +226,19 @@ The SDK calls Absurd stored procedures directly:
102226

103227
Retry, cancellation, idempotency, event wakeups, leases, and cleanup remain database-owned.
104228

105-
## Current status
229+
## Configuration
106230

107-
Experimental community SDK. The core typed API, worker, step/sleep/event helpers, cancellation mapping, idempotent spawn support, cleanup, and unknown-task deferral are implemented.
231+
`Client::from_env()` and `Client::from_env_queue()` resolve the database connection in this order:
108232

109-
Next hardening targets:
233+
1. `ABSURD_DATABASE_URL`
234+
2. `DATABASE_URL`
235+
3. `PGDATABASE`
236+
4. `postgresql://localhost/absurd`
110237

111-
- Integration tests ported from the Python and Go SDKs.
112-
- Task-result polling APIs when targeting Absurd SQL versions that expose `get_task_result`.
113-
- Optional spawn/execution middleware for tracing/header propagation.
114-
- Broader TLS/pool configuration.
115-
116-
Environment resolution uses `ABSURD_DATABASE_URL`, then `DATABASE_URL`, then `PGDATABASE`, then `postgresql://localhost/absurd`.
238+
A `PGDATABASE` value that is not a URL or keyword connection string is treated as a database name (`dbname=...`). Built-in URL constructors currently use `NoTls`; use `Client::from_pool` to provide a custom `deadpool_postgres::Pool`.
117239

118240
## Development
119241

120-
The crate targets Rust 1.85+ and edition 2024.
121-
122242
```sh
123243
cargo fmt --all --check
124244
cargo clippy --all-targets --all-features -- -D warnings
@@ -128,6 +248,12 @@ cargo deny check advisories bans licenses sources
128248
cargo package
129249
```
130250

251+
Database integration tests are ignored by default because they require a Postgres database initialized with Absurd SQL:
252+
253+
```sh
254+
ABSURD_DATABASE_URL=postgresql://localhost/absurd_test cargo test --test integration -- --ignored
255+
```
256+
131257
`Cargo.lock` is intentionally not committed because this is a library crate; CI resolves the current compatible dependency graph and Dependabot tracks manifest/action updates.
132258

133259
## License

0 commit comments

Comments
 (0)