A task management API demonstrating queue-based background processing with database persistence. HTTP endpoints accept tasks, a memory queue dispatches them, and concurrent workers process and store results in SQLite.
HTTP Client Queue Workers Database
│ │ │ │
│ POST /tasks │ │ │
│ {action, data} │ │ │
│ ────────────────────────────▶│ │ │
│ 202 {id, "queued"} │ queue.publish() │ │
│ │───────────────────────▶│ Worker 1 │
│ │───────────────────────▶│ Worker 2 │
│ │ │ process task │
│ │ │ INSERT result ───▶│
│ │ │ return true (ack) │
│ │ │ │
│ GET /tasks │ │ │
│ ──────────────────────────────────────────────────────────────────────────▶│
│ {tasks: [...], count: N} │ │ │ SELECT
- POST /tasks — validates request, generates UUID, publishes to queue, returns 202
- Queue consumer — 2 concurrent workers pull from the queue and process tasks
- Workers — execute the action (uppercase, sum, etc.), store result in SQLite
- GET /tasks — reads completed tasks from database, supports
?status=filter
14-task-queue/
├── wippy.lock
├── test.http # REST client requests for manual testing
├── k6.js # k6 load test script
└── src/
├── _index.yaml # db, queue, http, migration, handlers, consumer
├── migrate.lua # Creates tasks table (one-shot process)
├── create_task.lua # POST /tasks — queue publisher
├── list_tasks.lua # GET /tasks — database reader
└── process_task.lua # Queue worker — processes and stores results
| Entry | Kind | Purpose |
|---|---|---|
app:db |
db.sql.sqlite |
SQLite database (in-memory) |
app:queue_driver |
queue.driver.memory |
In-memory queue driver |
app:tasks_queue |
queue.queue |
Logical queue bound to driver |
app:processes |
process.host |
Hosts the migration process |
app:gateway |
http.service |
HTTP server on :8080 |
app:router |
http.router |
Route dispatcher |
app:migrate |
process.lua |
Migration: creates tasks table |
app:migrate_service |
process.service |
Auto-starts migration (exits on success) |
app:create_task |
function.lua |
POST /tasks handler |
app:list_tasks |
function.lua |
GET /tasks handler |
app:process_task |
function.lua |
Queue worker function |
app:task_consumer |
queue.consumer |
Consumer: 2 workers, prefetch 5 |
cd examples/14-task-queue
wippy init
wippy run# Create an uppercase task
curl -X POST http://localhost:8080/tasks \
-H "Content-Type: application/json" \
-d '{"action": "uppercase", "data": {"text": "hello world"}}'
# → {"id":"...","status":"queued"}
# Create a sum task
curl -X POST http://localhost:8080/tasks \
-H "Content-Type: application/json" \
-d '{"action": "sum", "data": {"numbers": [1, 2, 3, 4, 5]}}'
# → {"id":"...","status":"queued"}
# List all tasks (after processing)
curl http://localhost:8080/tasks
# → {"tasks":[...], "count":2}
# Filter by status
curl "http://localhost:8080/tasks?status=completed"Also available as test.http for REST client (VS Code, IntelliJ, etc.).
k6 run k6.jsTwo concurrent scenarios over 80 seconds:
- blast —
ramping-arrival-rate: ramps 500 → 5,000 → 10,000 rps task submissions (up to 5,000 VUs) - readers —
constant-arrival-rate: 100 rps steady GET /tasks polling
Thresholds: p95 submit < 300ms, p95 list < 500ms, < 1% failures.
| Action | Input | Output |
|---|---|---|
uppercase |
{"text": "hello"} |
{"output": "HELLO"} |
sum |
{"numbers": [1, 2, 3]} |
{"output": 6} |
| (other) | any | {"output": "processed"} |
queue.publish(queue_id, data)— publishes a message to a named queue. The data is serialized and delivered to a consumer worker.queue.consumer— entry kind that binds a queue to a handler function.concurrency: 2runs two parallel workers.prefetch: 5buffers up to 5 messages.queue.message()— accesses the current message context inside a consumer handler. Providesid()andheaders()methods.- Return
true/false— returningtruefrom a worker acknowledges the message (removed from queue). Returningfalsenacks it (requeued for retry). db.sql.sqlite— SQLite database entry. Using:memory:for this example (no persistence across restarts).sql.get(id)— acquires a database connection. Must calldb:release()when done.sql.builder— fluent query builder:sql.builder.select(...),sql.builder.insert(...). Chain with:from(),:where(),:order_by(),:limit(),:columns(),:values().query:run_with(db):query()— executes a SELECT and returns rows.query:run_with(db):exec()— executes INSERT/UPDATE/DELETE.- Migration pattern — a
process.lua+process.servicethat runs once on startup. Returns0to signal success; the supervisor won't restart it.