# "Task Queue" _Path: en/tutorials/task-queue_ > "Build a REST API that queues tasks for background processing with database persistence." ## Table of Contents - Task Queue ## Content # Task Queue Build a REST API that publishes tasks to an in-memory queue, processes them in background workers, and stores completed results in SQLite. **Classification:** Runnable tutorial. The page provides the complete registry, Lua sources, startup commands, and HTTP checks for a local single-node demo. ## Overview This tutorial creates a task management API demonstrating: - **REST endpoints** — Submit tasks and list results - **Queue publishing** — Dispatch jobs asynchronously - **Queue consumers** — Process jobs in background workers - **Database persistence** — Store completed results in SQLite - **Schema setup** — Create the database table in a one-shot process ```mermaid flowchart LR subgraph api["HTTP Server"] POST["/tasks POST"] GET["/tasks GET"] end subgraph queue["Queue"] Q[("tasks queue")] end subgraph workers["Workers"] W1["Consumer 1"] W2["Consumer 2"] end subgraph storage["Storage"] DB[(SQLite)] end POST -->|publish| Q Q --> W1 Q --> W2 W1 -->|INSERT| DB W2 -->|INSERT| DB GET -->|SELECT| DB ``` ## Prerequisites - Wippy runtime `v0.3.32a`. - `curl` or another HTTP client. - An empty working directory. Create the project and source directory before adding the files below: ```bash mkdir task-queue cd task-queue mkdir src ``` ## Project Structure ``` task-queue/ ├── wippy.lock ├── data/ # created before startup └── src/ ├── _index.yaml ├── migrate.lua ├── create_task.lua ├── list_tasks.lua └── process_task.lua ``` ## Entry Definitions Create `src/_index.yaml`: ```yaml version: "1.0" namespace: app entries: # Capabilities used by the tutorial's Lua entries in strict mode - name: runtime_policy kind: security.policy policy: actions: - db.get - queue.publish - queue.publish.queue resources: "*" effect: allow # SQLite database - name: db kind: db.sql.sqlite file: "./data/tasks.db" lifecycle: auto_start: true # Access policy for handlers, workers, and the migration - name: task_policy kind: security.policy policy: actions: - db.get - queue.publish - queue.publish.queue resources: "*" effect: allow # Memory queue driver - name: queue_driver kind: queue.driver.memory lifecycle: auto_start: true # Tasks queue - name: tasks_queue kind: queue.queue driver: app:queue_driver # HTTP server - name: gateway kind: http.service addr: ":8080" lifecycle: auto_start: true # Router - name: router kind: http.router meta: server: app:gateway # Migration process (runs once, exits) - name: migrate kind: process.lua source: file://migrate.lua method: main modules: - sql - logger security: actor: id: "service:migrate" policies: - app:task_policy # Migration service (auto-starts, exits on success) - name: migrate-service kind: process.service process: app:migrate host: app:processes lifecycle: auto_start: true # Process host - name: processes kind: process.host lifecycle: auto_start: true # API handlers - name: create_task kind: function.lua source: file://create_task.lua method: handler modules: - http - queue - uuid security: actor: id: "service:api" policies: - app:task_policy - name: list_tasks kind: function.lua source: file://list_tasks.lua method: handler modules: - http - sql security: actor: id: "service:api" policies: - app:task_policy # Queue worker - name: process_task kind: function.lua source: file://process_task.lua method: main modules: - sql - logger - json security: actor: id: "service:worker" policies: - app:task_policy # Endpoints - name: create_task.endpoint kind: http.endpoint meta: router: app:router method: POST path: /tasks func: app:create_task - name: list_tasks.endpoint kind: http.endpoint meta: router: app:router method: GET path: /tasks func: app:list_tasks # Queue consumer - name: task_consumer kind: queue.consumer queue: app:tasks_queue func: app:process_task concurrency: 2 prefetch: 5 lifecycle: auto_start: true ``` Strict mode is on by default, so an entry that reaches the database or the queue needs an actor and a scope. The `security:` block on each Lua entry supplies both from `app:task_policy`. See [Security Model](system/security.md). ## Migration Process Create `src/migrate.lua`: ```lua local sql = require("sql") local logger = require("logger") local function main() local db, err = sql.get("app:db") if err then logger:error("failed to connect", {error = tostring(err)}) error("failed to connect: " .. tostring(err)) end local _, exec_err = db:execute([[ CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, payload TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', result TEXT, created_at INTEGER NOT NULL, processed_at INTEGER ) ]]) db:release() if exec_err then logger:error("migration failed", {error = tostring(exec_err)}) error("migration failed: " .. tostring(exec_err)) end logger:info("migration complete") return 0 end return { main = main } ``` A normal return ends a `process.service` child without a restart; the supervisor retries only when the process raises an error. Returning `0` also maps to a successful exit status when the same process is launched as a CLI command. ## Create Task Endpoint Create `src/create_task.lua`: ```lua local http = require("http") local queue = require("queue") local uuid = require("uuid") local function handler() local req = http.request() local res = http.response() local body, parse_err = req:body_json() if parse_err then res:set_status(http.STATUS.BAD_REQUEST) res:write_json({error = "invalid JSON"}) return end if not body.action then res:set_status(http.STATUS.BAD_REQUEST) res:write_json({error = "action required"}) return end local task_id = uuid.v4() local task = { id = task_id, action = body.action, data = body.data or {}, created_at = os.time() } local ok, err = queue.publish("app:tasks_queue", task) if err then res:set_status(http.STATUS.INTERNAL_ERROR) res:write_json({error = "failed to queue task"}) return end res:set_status(http.STATUS.ACCEPTED) res:write_json({ id = task_id, status = "queued" }) end return { handler = handler } ``` ## List Tasks Endpoint Create `src/list_tasks.lua`: ```lua local http = require("http") local sql = require("sql") local function handler() local req = http.request() local res = http.response() local db, db_err = sql.get("app:db") if db_err then res:set_status(http.STATUS.INTERNAL_ERROR) res:write_json({error = "database unavailable"}) return end local status_filter = req:query("status") local query = sql.builder.select("id", "payload", "status", "result", "created_at", "processed_at") :from("tasks") :order_by("created_at DESC") :limit(100) if status_filter then query = query:where({status = status_filter}) end local rows, query_err = query:run_with(db):query() db:release() if query_err then res:set_status(http.STATUS.INTERNAL_ERROR) res:write_json({error = "query failed"}) return end res:set_status(http.STATUS.OK) res:write_json({ tasks = rows, count = #rows }) end return { handler = handler } ``` ## Queue Worker Create `src/process_task.lua`: ```lua local sql = require("sql") local logger = require("logger") local json = require("json") local function main(task) logger:info("processing task", { id = task.id, action = task.action }) local result if task.action == "uppercase" then result = {output = string.upper(task.data.text or "")} elseif task.action == "sum" then local nums = task.data.numbers or {} local total = 0 for _, n in ipairs(nums) do total = total + n end result = {output = total} else result = {output = "processed"} end local db, db_err = sql.get("app:db") if db_err then error("database unavailable: " .. tostring(db_err)) end local _, exec_err = db:execute( "INSERT OR REPLACE INTO tasks (id, payload, status, result, created_at, processed_at) VALUES (?, ?, ?, ?, ?, ?)", { task.id, json.encode(task), "completed", json.encode(result), task.created_at, os.time() } ) db:release() if exec_err then error("failed to store result: " .. tostring(exec_err)) end logger:info("task completed", {id = task.id}) end return { main = main } ``` The consumer auto-acks when the handler returns normally and auto-nacks when it raises an error. Call `msg:ack()` or `msg:nack()` via `queue.message()` only when you need explicit control before the handler ends. ## Running the Service Create the data directory, initialize the project, and start the runtime: ```bash mkdir data wippy init wippy run ``` Leave the runtime running while you use a second terminal for the HTTP checks. Wait until the logs report that the HTTP service is listening and the migration completed; the one-shot migration and the HTTP service start independently during boot. Submit a task and query its result: ```bash # Create a task curl -X POST http://localhost:8080/tasks \ -H "Content-Type: application/json" \ -d '{"action": "uppercase", "data": {"text": "hello world"}}' # Wait a moment for processing, then list tasks curl http://localhost:8080/tasks # Filter by status curl "http://localhost:8080/tasks?status=completed" ``` The returned row should have `status: "completed"`; its `result` field is a JSON string containing `{"output":"HELLO WORLD"}`. The in-memory queue is intentionally non-durable, but completed rows survive restarts in `data/tasks.db`. ## Troubleshooting and Cleanup - `no such table: tasks` means the request reached SQLite before the migration finished. Wait for `migration complete` and retry. A migration error stops the migration service and is shown in the runtime logs. - `failed to queue task` usually means `app:queue_driver` or `app:task_consumer` did not start. Check the startup logs for the first resource error rather than retrying the request. - `address already in use` means another process owns port 8080. Stop it or change `app:gateway.addr` and use the same port in the `curl` commands. - Stop the runtime with Ctrl+C. Remove `data/tasks.db` to reset the tutorial data; the next start recreates the schema. ## Message Flow 1. **POST /tasks** receives the request, generates a UUID, and publishes the task. 2. A **queue consumer** receives the message; up to two handlers run concurrently. 3. The **worker** processes the task and writes its result to SQLite. 4. **GET /tasks** reads completed tasks from the database. ## Next Steps - [HTTP Module](lua/http/http.md) — Request and response handling - [Queue Module](lua/storage/queue.md) — Message queue operations - [SQL Module](lua/storage/sql.md) — Database access - [Queue Consumers](guides/queue-consumers.md) — Queue configuration ## Navigation Previous: "Process Supervision Recipes" (tutorials/supervision) Next: "Testing" (tutorials/testing)