Skip to content

Repository files navigation

OpenWorkers Task Executor

A minimal, standalone task executor that runs tasks on the runtime backend picked at build time. Unlike the full OpenWorkers runner, this executor:

  • Has minimal dependencies
  • Only implements fetch() (no KV, Storage, Database bindings)
  • Supports multiple task sources: CLI, NATS, PostgreSQL

Perfect for background jobs, scheduled tasks, or lightweight serverless workloads.

Runtime backends

Exactly one backend feature must be selected; the build fails otherwise.

Backend Feature Snapshots Guest fetch() Known limitations
V8 v8 yes yes Needs a prebuilt librusty_v8 or a V8 source build
JSC jsc no yes System JavaScriptCore; a bridge wires export default to the task listener
QuickJS quickjs no yes Task rides on a fetch event; enforces no CPU limit
Boa boa no yes Task rides on a fetch event; enforces no CPU limit; bodies round-trip as lossy UTF-8
Nova nova no no No host operations at all, and no timers; the deadline is not enforced; no Temporal, see below
Wasm wasm no yes Guests are wasi:http/proxy components, not JavaScript; see Wasm guests

One lockfile serves every backend, so nova gives up Temporal: v8 152 needs temporal_capi >= 0.2.3, which pulls icu_calendar 2.2.1, while the temporal_rs 0.1.2 behind nova_vm 1.0's default temporal feature compiles only against icu_calendar 2.1. nova_vm 1.0.0 in turn does not build without that feature, which is why the backend takes its engine from openworkers/nova, the 1.0.0 release plus the seven cfg attributes that gate it.

Features

Feature Description Default
nats NATS message queue listener yes
database PostgreSQL queue with pg_notify no

Installation

# Build against a backend
cargo build --release --features v8

# Build with database support
cargo build --release --features v8,database

# The V8 build takes a prebuilt archive rather than building V8 from source
RUSTY_V8_ARCHIVE=~/rusty-v8-prebuilt/librusty_v8_ptrcomp_release_aarch64-apple-darwin.a \
RUSTY_V8_SRC_BINDING_PATH=~/rusty-v8-prebuilt/src_binding_ptrcomp_release_aarch64-apple-darwin.rs \
cargo build --release --features v8

V8 Snapshot (Important)

For optimal performance and stability, generate a V8 snapshot before running:

cargo run --features v8 --bin snapshot

This creates a snapshot at /tmp/openworkers-runtime-snapshot.bin containing pre-compiled JavaScript APIs (URL, Headers, Request, Response, etc.).

Without a snapshot:

  • Slower cold starts (~2-3ms vs ~100us)
  • APIs are compiled on every request

Docker: The Docker image includes the snapshot automatically.

The other backends have no snapshot support, and the snapshot binary is only built with --features v8.

Usage

One-shot execution (run)

Execute a single JavaScript file:

# Simple execution
task-executor run script.js

# With JSON payload
task-executor run script.js --payload '{"name": "world"}'

# With timeout (ms)
task-executor run script.js --timeout 5000

# Quiet mode (suppress console.log)
task-executor run script.js --quiet

Example script:

export default {
  async task(event) {
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();

    return {
      input: event.payload,
      result: data,
    };
  },
};

NATS listener (listen)

Listen for tasks on a NATS subject:

task-executor listen \
  --nats nats://localhost:4222 \
  --subject tasks \
  --root ./workers \
  --timeout 30000

NATS Message Format

{
  "script": "hello.js",
  "payload": { "name": "world" },
  "timeout": 5000
}

Note: Scripts must exist in the --root directory. Nested paths like "script": "workers/task.js" are allowed.

Database listener (db-listen)

Listen for tasks from a PostgreSQL table using pg_notify:

task-executor db-listen \
  --database-url postgres://user:pass@localhost/mydb \
  --table ow_tasks \
  --root ./workers \
  --timeout 30000

Environment Variables

Variable Description Default
DATABASE_URL PostgreSQL connection URL (required)
TASK_TABLE Name of the tasks table ow_tasks

SQL Schema

Apply this schema to your database (adjust table name as needed):

CREATE TABLE ow_tasks (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    script TEXT NOT NULL,
    payload JSONB,
    status TEXT NOT NULL DEFAULT 'pending',
    result JSONB,
    error TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    started_at TIMESTAMPTZ,
    completed_at TIMESTAMPTZ
);

-- Index for efficient pending task lookup
CREATE INDEX idx_ow_tasks_pending ON ow_tasks(created_at) WHERE status = 'pending';

-- Notification trigger
CREATE OR REPLACE FUNCTION notify_ow_task_created() RETURNS TRIGGER AS $$
BEGIN
    PERFORM pg_notify('ow_tasks_created', NEW.id::text);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER ow_task_notify_insert
    AFTER INSERT ON ow_tasks
    FOR EACH ROW EXECUTE FUNCTION notify_ow_task_created();

Note: If you use a custom table name, update the trigger to notify on {table_name}_created.

Inserting Tasks

-- Script must exist in the --root directory
INSERT INTO ow_tasks (script, payload)
VALUES ('hello.js', '{"name": "world"}');

-- Nested paths are allowed
INSERT INTO ow_tasks (script, payload)
VALUES ('workers/process.js', '{"data": [1, 2, 3]}');

Task Lifecycle

  1. pending -> Task created, waiting for pickup
  2. running -> Task claimed by executor, started_at set
  3. completed -> Success, result contains return value
  4. failed -> Error, error contains message

Script API

Scripts export a default object with a task(event, env, ctx) method. Module syntax is lowered to a classic script before evaluation, so export default becomes globalThis.default.

export default {
  async task(event, env, ctx) {
    // event.payload is the JSON payload passed to the task
    // event.taskId, event.source and event.attempt describe the invocation

    // Use fetch() for HTTP requests
    const response = await fetch("https://api.example.com");

    // Return value is stored as the task result
    return {
      status: "done",
      data: await response.json(),
    };
  },
};

A listener works too, and takes precedence over the default export:

addEventListener("task", (event) => {
  event.respondWith({ success: true, data: { received: event.payload } });
  event.waitUntil(reportLater());
});

A returned value that carries a boolean success is taken as the task result itself; anything else becomes its data.

Wasm guests

The wasm backend runs WebAssembly components, not JavaScript. A component has no task export, so a task is delivered as a synthetic POST http://task.invalid/ to wasi:http/incoming-handler: the payload is the request body, the response body is the task result, and x-ow-task-id, x-ow-task-attempt, x-ow-task-source and x-ow-task-origin carry the metadata. Outbound wasi:http/outgoing-handler requests go through the same fetch handler as the JavaScript backends.

(cd examples/task-worker && cargo build --target wasm32-wasip2 --release)

cargo run --features wasm --bin task-executor -- \
  run examples/task-worker/target/wasm32-wasip2/release/task_worker.wasm \
  --payload '{"n":21}'

Available APIs

What a guest gets depends on the backend; this is the V8 set.

API Description
fetch() Standard Fetch API for HTTP requests
Request / Response Fetch API request/response classes
Headers HTTP headers manipulation
URL / URLSearchParams URL parsing and manipulation
TextEncoder/Decoder UTF-8 encoding/decoding
atob() / btoa() Base64 encoding/decoding
crypto.randomUUID() Generate random UUIDs
crypto.getRandomValues Cryptographic random values
AbortController Request cancellation
Blob / FormData Binary data and form handling
console.log/warn/error Logging (printed to stderr)
setTimeout/setInterval Timers (within task execution)

Logging

Enable debug logging with:

RUST_LOG=debug task-executor run script.js

Multiple Workers

The database listener supports running multiple instances concurrently. Tasks are claimed using SELECT FOR UPDATE SKIP LOCKED, ensuring each task is processed exactly once.

# Terminal 1
task-executor db-listen --database-url $DATABASE_URL

# Terminal 2
task-executor db-listen --database-url $DATABASE_URL

# Both will process tasks without conflicts

Testing

# Run the suite against a backend
cargo test --features v8

# The wasm tests need the example component built first
(cd examples/task-worker && cargo build --target wasm32-wasip2 --release)
cargo test --features wasm

# Run only database tests (requires PostgreSQL)
cargo test --features v8,database db_

Database tests use .env.test for configuration:

# .env.test
DATABASE_URL=postgres://postgres:postgres@localhost/postgres

Tests create isolated tables (test_tasks_{uuid}) and clean up automatically.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages