OAKENMAIL
Pre-alpha

Events, jobs and the scheduler

An in-memory event bus tells the rest of core what happened. A durable job queue does the work, and survives a crash. A scheduler puts recurring work on that queue.

1

The three pieces

Piece Lives in Survives a restart? Job
Event bus memory no tell listeners that something happened
Job queue raw.db yes run work at least once, with retries
Scheduler raw.db yes enqueue recurring work on time, once

Nothing here calls a model or a mail provider. Ingest, sync and enrichment arrive in later prompts as job handlers; this layer only runs them.

2

The job lifecycle

A job is a row in raw.db. Workers take jobs by claiming them: one short transaction marks the best runnable job as running and gives the worker a lease. A worker that dies simply stops renewing its lease; once the lease expires, the job is queued again.

sequenceDiagram
    participant P as Producer
    participant Q as Queue (raw.db)
    participant W as Worker
    participant H as Handler
    P->>Q: enqueue(kind, payload, dedupe_key)
    Q-->>P: job id (or the existing one)
    W->>Q: claim(kinds, limits)
    Note over Q: reclaim expired leases, skip kinds at their limit,<br/>pick highest priority, set lease, attempts + 1
    Q-->>W: job
    W->>H: handler(payload, job)
    loop while it runs
        W->>Q: heartbeat (extends the lease)
    end
    alt success
        H-->>W: returns
        W->>Q: complete, status succeeded
    else error and attempts left
        H-->>W: raises
        W->>Q: fail, queued again after backoff
        Note over Q: delay doubles each attempt, capped, with jitter
    else permanent error or out of attempts
        W->>Q: fail, status failed (last_error kept)
    end

States are queued, running, succeeded, failed and cancelled. Every change is also published as a JobStateChanged event.

3

Guarantees and rules

4

Events

The bus carries typed, frozen events: sync progress, message stored, parsed, indexed, embedded, enriched, read-state changes, job state changes and plugin health. There are two kinds of listener:

5

Results, parents and actors

A job can store a result, which is checked against a schema for its kind before it is saved. It can name a parent job, for splitting work into pieces, and it records its actor (user, agent or system). Besides core's own workers, external agents can lease jobs too: see the swarm.

6

The scheduler

A schedule is either a fixed interval or a five-field cron expression, evaluated in UTC (no daylight-saving surprises). Its next_run is stored in raw.db. A tick enqueues the due jobs and advances next_run in one transaction, so a restart neither repeats a run nor skips one. If the process was down for several periods, the missed runs collapse into a single run.

The cron parser is about a hundred lines with no dependency. It covers *, lists, ranges, steps and month and weekday names, which is everything "sync every five minutes" or "nightly backfill" needs; a full library would add a dependency and time-zone handling that nothing here uses.

7

Testing

Everything above is tested with a fake clock and no sleeping: advance the clock, call drain() on the runner or tick() on the scheduler, and look at the database. The tests live in src/oaken/core/tests/.