Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 76 additions & 92 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,19 +2,21 @@

Worker is a FreeRTOS task and cooperative job execution library for ESP32.

Worker helps you run one-off and recurring background work in Arduino ESP32 projects with explicit task configuration, cooperative stop/sleep controls, event reporting, and diagnostics. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the app.
Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application.

[![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md)

## Why use Worker?

* **Task-per-job execution** - each `once()` and `every()` job owns a FreeRTOS task.
* **Safe recurring jobs** - `every()` applies the interval delay internally after each callback.
* **ESP32 task control** - configure byte stack size, priority, core affinity, and stack memory preference.
* **Cooperative lifecycle** - jobs can stop or sleep through `WorkerJobContext`.
* **Production-minded** - result-based errors, event callbacks, diagnostics, thread-safe internals, and no intentional exceptions.
- **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task.
- **Automatic cleanup** — callers never need to reap completed jobs.
- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`.
- **Safe recurring jobs** — `every()` applies the interval after each callback.
- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference.
- **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`.
- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history.

## Install

Expand All@@ -27,19 +29,17 @@ board = esp32dev
framework = arduino

lib_deps =
https://github.com/ZekStack/worker.git
https://github.com/ZekStack/worker.git

build_flags =
-std=gnu++20
-std=gnu++20
build_unflags =
-std=gnu++11
-std=gnu++11
```

### Arduino IDE

Worker is not published to Arduino Library Manager yet.

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.
Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory.

```txt
Arduino/libraries/Worker
Expand All@@ -55,60 +55,79 @@ Worker worker;
WorkerJobId recurringJob = 0;

void setup() {
Serial.begin(115200);

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
return;
}

worker.once([](WorkerJobContext &ctx) {
Serial.printf("one-off job id=%u\n", static_cast<unsigned>(ctx.id()));
});

WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {
Serial.printf("run=%u\n", static_cast<unsigned>(ctx.runCount()));
if (ctx.runCount() >= 5) {
ctx.stop();
}
});

if (result) {
recurringJob = result.jobId;
}
Serial.begin(115200);

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
return;
}

worker.once([](WorkerJobContext &ctx) {
Serial.printf("one-off job id=%u\n", static_cast<unsigned>(ctx.id()));
});

WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {
Serial.printf("run=%u\n", static_cast<unsigned>(ctx.runCount()));
if (ctx.runCount() >= 5) {
ctx.stop();
}
});

if (result) {
recurringJob = result.jobId;
}
}

void loop() {
delay(1000);
delay(1000);
}
```

No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically.

## Cleanup model

Worker creates one long-lived internal cleanup task during `init()`.

When a job callback finishes, the job:

1. releases its stored callback;
2. queues its handle and immutable allocation type;
3. suspends itself.

The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns.

This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself.

`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup.

## Important notes

> [!IMPORTANT]
> Worker cancellation is cooperative. `stop()` requests that a job stops and wakes it if it is sleeping, but it does not force-delete a running callback.

* A callback that blocks forever will prevent `stopAndWait()` and timed `end()` calls from completing before their timeout. The destructor waits without a timeout so tasks cannot outlive Worker internals.
* `every(intervalMs, callback)` delays internally after each callback.
* Completed jobs are retained until `waitFor()` can consume them after task cleanup, `clearFinished()` is called, or Worker ends.
* Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
* Custom task names are copied into a fixed internal buffer and may be truncated.
* `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
* `WorkerEvent::type` clearly identifies `Info`, `Warning`, or `Error` events.
* Worker APIs use result objects for normal failures. Catastrophic STL allocation failure while constructing result messages, callbacks, or internal containers is not recoverable by Worker on platforms where the standard library throws or aborts.
> Worker cancellation is cooperative. `stop()` requests that a job stops and wakes it if it is sleeping, but it does not interrupt a running callback.

- A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing.
- The destructor waits without a timeout so tasks cannot outlive Worker internals.
- `every(intervalMs, callback)` delays after each callback.
- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
- `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity.
- `clearFinished()` is retained only as a deprecated compatibility no-op.
- Completion synchronization uses a small bounded token window and never retains callbacks or full completed records.
- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts.

## Examples

| Example | Description |
| --- | --- |
| `Basic` | Minimal init, one-off job, recurring job, wait, and cooperative stop. |
| `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. |
| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. |
| `Events` | Event callback and error event handling. |
| `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. |
| `Diagnostics` | Aggregate diagnostics and active per-job diagnostics. |
| `Diagnostics` | Current job and cleanup-task diagnostics. |
| `BindableCallbacks` | `std::bind` with private class methods. |
| `TaskCleanupSentinel` | Runtime sentinel for verifying task-entry C++ cleanup before `waitFor()` returns. |
| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. |

Start with:

Expand All@@ -118,15 +137,13 @@ examples/Basic

## Documentation

Detailed documentation is available in the `docs/` folder.

| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Step-by-step setup and first job flow. |
| [`docs/configuration.md`](docs/configuration.md) | Config options and stack behavior. |
| [`docs/api.md`](docs/api.md) | Public classes, result types, events, and diagnostics. |
| [`docs/examples.md`](docs/examples.md) | Explanation of all included examples. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common issues and solutions. |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. |
| [`docs/examples.md`](docs/examples.md) | Example descriptions. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. |

## API overview

Expand All@@ -139,62 +156,29 @@ WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {});
WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {});

worker.sleep(loop.jobId, 5000);
worker.stopAndWait(loop.jobId, 2000);

WorkerDiag diag = worker.getDiagnostics();
WorkerJobDiag jobDiag;
worker.getJobDiagnostics(loop.jobId, jobDiag);

worker.stopAndWait(loop.jobId, 2000);
worker.clearFinished();
```

For the full API, see [`docs/api.md`](docs/api.md).

## Compatibility

| Item | Support |
| --- | --- |
| Framework | Arduino ESP32 |
| Platform | `espressif32` |
| Platform | `espressif32` / PIOArduino |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional for task stacks when ESP-IDF support is available |
| PSRAM | Optional task stacks through ESP-IDF capability APIs |
| Dependencies | none |
| Exceptions | Not used |
| Status | Early-stage `0.1.0` |

## Configuration

```cpp
WorkerConfig config;
config.defaultStackSize = 4096;
config.defaultPriority = 1;
config.defaultCoreId = tskNO_AFFINITY;
config.defaultStackType = WorkerStackType::Auto;

WorkerResult result = worker.init(config);
```

For all options, see [`docs/configuration.md`](docs/configuration.md).

## Error handling

Worker reports operation status through `WorkerResult` and `WorkerJobResult`.

```cpp
WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {});

if (!result) {
Serial.println(result.message.c_str());
return;
}
```

For result fields and status codes, see [`docs/api.md`](docs/api.md).

## License

MIT - see [`LICENSE.md`](LICENSE.md).
MIT see [`LICENSE.md`](LICENSE.md).

## ZekStack

Expand Down
60 changes: 39 additions & 21 deletions docs/api.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,36 +6,48 @@ This page summarizes the public API declared in `src/Worker.h`.

Worker does not intentionally throw exceptions. Operations report normal failures through `WorkerResult` or `WorkerJobResult`.

Catastrophic STL allocation failure while constructing result messages, callbacks, or internal containers is not recoverable by Worker on platforms where the standard library throws or aborts.

| Field | Meaning |
| --- | --- |
| `result` | `true` on success, `false` on failure. |
| `status` | Machine-readable `WorkerStatus`. |
| `message` | Human-readable status. |
| `jobId` | Returned by `WorkerJobResult` when a job was created or partially allocated. |
| `jobId` | Returned by `WorkerJobResult` after a job was created. |

`WorkerStatus` values are `Ok`, `NotInitialized`, `AlreadyInitialized`, `InvalidArgument`, `OutOfMemory`, `TaskCreateFailed`, `JobNotFound`, `Busy`, `Timeout`, and `InternalError`.

## Worker

| Method | Purpose |
| --- | --- |
| `init(config)` | Initialize Worker defaults. |
| `init(config)` | Initialize Worker and its cleanup task. |
| `onEvent(callback)` | Register a synchronous event callback. |
| `once(callback)` | Start a one-off task. |
| `once(config, callback)` | Start a configured one-off task. |
| `once(callback)` | Start an automatically cleaned one-off task. |
| `once(config, callback)` | Start a configured automatically cleaned one-off task. |
| `every(intervalMs, callback)` | Start a recurring task with internal delay. |
| `every(intervalMs, config, callback)` | Start a configured recurring task. |
| `stop(jobId)` | Request cooperative stop. |
| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until terminal state and task cleanup. |
| `stop(jobId)` | Request cooperative stop. Cleanup continues automatically. |
| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until the task stack and TCB are released. |
| `sleep(jobId, durationMs)` | Request that a job sleeps. |
| `waitFor(jobId)` | Wait until a registered or retained job reaches terminal state and task cleanup. |
| `waitFor(jobId, timeoutMs)` | Wait with timeout until terminal state and task cleanup. |
| `clearFinished()` | Reap retained terminal job records. |
| `getDiagnostics()` | Return aggregate lifetime diagnostics and current active counts. |
| `getJobDiagnostics(jobId, out)` | Fill per-job diagnostics for an active or retained terminal job. |
| `end(timeoutMs)` | Stop jobs and end Worker, returning `Timeout` if callbacks do not finish in time. |
| `waitFor(jobId)` | Optionally wait until physical task cleanup completes. |
| `waitFor(jobId, timeoutMs)` | Wait with timeout until physical task cleanup completes. |
| `clearFinished()` | Deprecated compatibility no-op. Worker cleans jobs automatically. |
| `getDiagnostics()` | Return current runtime and cleanup-task state. |
| `getJobDiagnostics(jobId, out)` | Fill diagnostics for a currently active job. |
| `end(timeoutMs)` | Stop jobs, drain cleanup, and stop Worker infrastructure. |

`once()` and `every()` are safe for fire-and-forget use. A caller never needs `waitFor()` or `clearFinished()` to release Worker-owned resources.

## Cleanup lifecycle

Worker owns every task it creates. A completed job follows this lifecycle:

1. The callback returns and its stored `std::function` is released.
2. The job queues its task handle to the Worker cleanup task.
3. The job task suspends itself.
4. The cleanup task deletes it externally with `vTaskDelete()` or `vTaskDeleteWithCaps()` as appropriate.
5. Worker records a small bounded completion token and removes the full active job record.

`waitFor()` and `stopAndWait()` succeed only after step 4. They are synchronization APIs, not cleanup APIs.

## Events

Expand All@@ -47,28 +59,34 @@ worker.onEvent([](WorkerEvent event) {
});
```

`WorkerEvent::type` is the source of truth for event severity. Error events use `WorkerEventType::Error` and include a `WorkerStatus`.
Completion events are emitted after physical task deletion completes.

## Job Context
## Job context

Callbacks receive `WorkerJobContext&`.

| Method | Purpose |
| --- | --- |
| `id()` | Return the current job id. |
| `id()` | Return the current job ID. |
| `stop()` | Request that the current job stops. |
| `sleep(durationMs)` | Sleep the current job cooperatively. |
| `shouldStop()` | Check the cooperative stop flag. |
| `runCount()` | Number of callback runs started. |
| `startedAtMs()` | First run time from `millis()`. |
| `lastRunAtMs()` | Most recent run time from `millis()`. |

The context is valid only during callback execution.

## Diagnostics

`WorkerDiag` reports aggregate job counts and stack diagnostics. `totalJobCount`, `finishedJobCount`, `stoppedJobCount`, `failedJobCount`, stack type counts, and total stack high-water data are lifetime counters since `init()`. `runningJobCount` and `sleepingJobCount` describe currently active jobs.
`WorkerDiag` reports current state only:

- active, running, sleeping, stopping, and cleanup-queued job counts;
- cleanup-task running state;
- cleanup queue depth and high-water mark.

Completed job records are retained after they reach `Finished`, `Stopped`, or `Failed`. `WorkerJobDiag` reports state, name, stack config, run count, timing, and stack high-water data while a job is active or retained. After `waitFor()` consumes the job following task cleanup, `clearFinished()` reaps it, or Worker ends, `getJobDiagnostics()` returns `JobNotFound`.
Worker does not retain lifetime job counters. `WorkerJobDiag` is available only while a job is active. After automatic cleanup removes the active record, `getJobDiagnostics()` returns `JobNotFound`.

`waitFor()` and `stopAndWait()` return success only after the job has reached a terminal state and its task entry has completed C++ cleanup. Callback captures and task-entry RAII objects have been released before the job is reaped.
A bounded internal completion window allows `waitFor()` to observe fast jobs after their active records have already been removed. It does not retain callbacks, task handles, names, or full diagnostics.

The Worker destructor performs the same cooperative shutdown without a timeout so running tasks cannot continue after Worker internals are destroyed.
The Worker destructor performs cooperative shutdown without a timeout so tasks cannot outlive Worker internals.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 76 additions & 92 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,19 +2,21 @@

Worker is a FreeRTOS task and cooperative job execution library for ESP32.

Worker helps you run one-off and recurring background work in Arduino ESP32 projects with explicit task configuration, cooperative stop/sleep controls, event reporting, and diagnostics. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the app.
Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application.

[![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md)

## Why use Worker?

* **Task-per-job execution** - each `once()` and `every()` job owns a FreeRTOS task.
* **Safe recurring jobs** - `every()` applies the interval delay internally after each callback.
* **ESP32 task control** - configure byte stack size, priority, core affinity, and stack memory preference.
* **Cooperative lifecycle** - jobs can stop or sleep through `WorkerJobContext`.
* **Production-minded** - result-based errors, event callbacks, diagnostics, thread-safe internals, and no intentional exceptions.
- **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task.
- **Automatic cleanup** — callers never need to reap completed jobs.
- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`.
- **Safe recurring jobs** — `every()` applies the interval after each callback.
- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference.
- **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`.
- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history.

## Install

Expand All@@ -27,19 +29,17 @@ board = esp32dev
framework = arduino

lib_deps =
https://github.com/ZekStack/worker.git
https://github.com/ZekStack/worker.git

build_flags =
-std=gnu++20
-std=gnu++20
build_unflags =
-std=gnu++11
-std=gnu++11
```

### Arduino IDE

Worker is not published to Arduino Library Manager yet.

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.
Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory.

```txt
Arduino/libraries/Worker
Expand All@@ -55,60 +55,79 @@ Worker worker;
WorkerJobId recurringJob = 0;

void setup() {
Serial.begin(115200);

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
return;
}

worker.once([](WorkerJobContext &ctx) {
Serial.printf("one-off job id=%u\n", static_cast<unsigned>(ctx.id()));
});

WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {
Serial.printf("run=%u\n", static_cast<unsigned>(ctx.runCount()));
if (ctx.runCount() >= 5) {
ctx.stop();
}
});

if (result) {
recurringJob = result.jobId;
}
Serial.begin(115200);

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
return;
}

worker.once([](WorkerJobContext &ctx) {
Serial.printf("one-off job id=%u\n", static_cast<unsigned>(ctx.id()));
});

WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {
Serial.printf("run=%u\n", static_cast<unsigned>(ctx.runCount()));
if (ctx.runCount() >= 5) {
ctx.stop();
}
});

if (result) {
recurringJob = result.jobId;
}
}

void loop() {
delay(1000);
delay(1000);
}
```

No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically.

## Cleanup model

Worker creates one long-lived internal cleanup task during `init()`.

When a job callback finishes, the job:

1. releases its stored callback;
2. queues its handle and immutable allocation type;
3. suspends itself.

The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns.

This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself.

`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup.

## Important notes

> [!IMPORTANT]
> Worker cancellation is cooperative. `stop()` requests that a job stops and wakes it if it is sleeping, but it does not force-delete a running callback.

* A callback that blocks forever will prevent `stopAndWait()` and timed `end()` calls from completing before their timeout. The destructor waits without a timeout so tasks cannot outlive Worker internals.
* `every(intervalMs, callback)` delays internally after each callback.
* Completed jobs are retained until `waitFor()` can consume them after task cleanup, `clearFinished()` is called, or Worker ends.
* Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
* Custom task names are copied into a fixed internal buffer and may be truncated.
* `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
* `WorkerEvent::type` clearly identifies `Info`, `Warning`, or `Error` events.
* Worker APIs use result objects for normal failures. Catastrophic STL allocation failure while constructing result messages, callbacks, or internal containers is not recoverable by Worker on platforms where the standard library throws or aborts.
> Worker cancellation is cooperative. `stop()` requests that a job stops and wakes it if it is sleeping, but it does not interrupt a running callback.

- A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing.
- The destructor waits without a timeout so tasks cannot outlive Worker internals.
- `every(intervalMs, callback)` delays after each callback.
- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
- `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity.
- `clearFinished()` is retained only as a deprecated compatibility no-op.
- Completion synchronization uses a small bounded token window and never retains callbacks or full completed records.
- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts.

## Examples

| Example | Description |
| --- | --- |
| `Basic` | Minimal init, one-off job, recurring job, wait, and cooperative stop. |
| `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. |
| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. |
| `Events` | Event callback and error event handling. |
| `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. |
| `Diagnostics` | Aggregate diagnostics and active per-job diagnostics. |
| `Diagnostics` | Current job and cleanup-task diagnostics. |
| `BindableCallbacks` | `std::bind` with private class methods. |
| `TaskCleanupSentinel` | Runtime sentinel for verifying task-entry C++ cleanup before `waitFor()` returns. |
| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. |

Start with:

Expand All@@ -118,15 +137,13 @@ examples/Basic

## Documentation

Detailed documentation is available in the `docs/` folder.

| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Step-by-step setup and first job flow. |
| [`docs/configuration.md`](docs/configuration.md) | Config options and stack behavior. |
| [`docs/api.md`](docs/api.md) | Public classes, result types, events, and diagnostics. |
| [`docs/examples.md`](docs/examples.md) | Explanation of all included examples. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common issues and solutions. |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. |
| [`docs/examples.md`](docs/examples.md) | Example descriptions. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. |

## API overview

Expand All@@ -139,62 +156,29 @@ WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {});
WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {});

worker.sleep(loop.jobId, 5000);
worker.stopAndWait(loop.jobId, 2000);

WorkerDiag diag = worker.getDiagnostics();
WorkerJobDiag jobDiag;
worker.getJobDiagnostics(loop.jobId, jobDiag);

worker.stopAndWait(loop.jobId, 2000);
worker.clearFinished();
```

For the full API, see [`docs/api.md`](docs/api.md).

## Compatibility

| Item | Support |
| --- | --- |
| Framework | Arduino ESP32 |
| Platform | `espressif32` |
| Platform | `espressif32` / PIOArduino |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional for task stacks when ESP-IDF support is available |
| PSRAM | Optional task stacks through ESP-IDF capability APIs |
| Dependencies | none |
| Exceptions | Not used |
| Status | Early-stage `0.1.0` |

## Configuration

```cpp
WorkerConfig config;
config.defaultStackSize = 4096;
config.defaultPriority = 1;
config.defaultCoreId = tskNO_AFFINITY;
config.defaultStackType = WorkerStackType::Auto;

WorkerResult result = worker.init(config);
```

For all options, see [`docs/configuration.md`](docs/configuration.md).

## Error handling

Worker reports operation status through `WorkerResult` and `WorkerJobResult`.

```cpp
WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {});

if (!result) {
Serial.println(result.message.c_str());
return;
}
```

For result fields and status codes, see [`docs/api.md`](docs/api.md).

## License

MIT - see [`LICENSE.md`](LICENSE.md).
MIT see [`LICENSE.md`](LICENSE.md).

## ZekStack

Expand Down
60 changes: 39 additions & 21 deletions docs/api.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,36 +6,48 @@ This page summarizes the public API declared in `src/Worker.h`.

Worker does not intentionally throw exceptions. Operations report normal failures through `WorkerResult` or `WorkerJobResult`.

Catastrophic STL allocation failure while constructing result messages, callbacks, or internal containers is not recoverable by Worker on platforms where the standard library throws or aborts.

| Field | Meaning |
| --- | --- |
| `result` | `true` on success, `false` on failure. |
| `status` | Machine-readable `WorkerStatus`. |
| `message` | Human-readable status. |
| `jobId` | Returned by `WorkerJobResult` when a job was created or partially allocated. |
| `jobId` | Returned by `WorkerJobResult` after a job was created. |

`WorkerStatus` values are `Ok`, `NotInitialized`, `AlreadyInitialized`, `InvalidArgument`, `OutOfMemory`, `TaskCreateFailed`, `JobNotFound`, `Busy`, `Timeout`, and `InternalError`.

## Worker

| Method | Purpose |
| --- | --- |
| `init(config)` | Initialize Worker defaults. |
| `init(config)` | Initialize Worker and its cleanup task. |
| `onEvent(callback)` | Register a synchronous event callback. |
| `once(callback)` | Start a one-off task. |
| `once(config, callback)` | Start a configured one-off task. |
| `once(callback)` | Start an automatically cleaned one-off task. |
| `once(config, callback)` | Start a configured automatically cleaned one-off task. |
| `every(intervalMs, callback)` | Start a recurring task with internal delay. |
| `every(intervalMs, config, callback)` | Start a configured recurring task. |
| `stop(jobId)` | Request cooperative stop. |
| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until terminal state and task cleanup. |
| `stop(jobId)` | Request cooperative stop. Cleanup continues automatically. |
| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until the task stack and TCB are released. |
| `sleep(jobId, durationMs)` | Request that a job sleeps. |
| `waitFor(jobId)` | Wait until a registered or retained job reaches terminal state and task cleanup. |
| `waitFor(jobId, timeoutMs)` | Wait with timeout until terminal state and task cleanup. |
| `clearFinished()` | Reap retained terminal job records. |
| `getDiagnostics()` | Return aggregate lifetime diagnostics and current active counts. |
| `getJobDiagnostics(jobId, out)` | Fill per-job diagnostics for an active or retained terminal job. |
| `end(timeoutMs)` | Stop jobs and end Worker, returning `Timeout` if callbacks do not finish in time. |
| `waitFor(jobId)` | Optionally wait until physical task cleanup completes. |
| `waitFor(jobId, timeoutMs)` | Wait with timeout until physical task cleanup completes. |
| `clearFinished()` | Deprecated compatibility no-op. Worker cleans jobs automatically. |
| `getDiagnostics()` | Return current runtime and cleanup-task state. |
| `getJobDiagnostics(jobId, out)` | Fill diagnostics for a currently active job. |
| `end(timeoutMs)` | Stop jobs, drain cleanup, and stop Worker infrastructure. |

`once()` and `every()` are safe for fire-and-forget use. A caller never needs `waitFor()` or `clearFinished()` to release Worker-owned resources.

## Cleanup lifecycle

Worker owns every task it creates. A completed job follows this lifecycle:

1. The callback returns and its stored `std::function` is released.
2. The job queues its task handle to the Worker cleanup task.
3. The job task suspends itself.
4. The cleanup task deletes it externally with `vTaskDelete()` or `vTaskDeleteWithCaps()` as appropriate.
5. Worker records a small bounded completion token and removes the full active job record.

`waitFor()` and `stopAndWait()` succeed only after step 4. They are synchronization APIs, not cleanup APIs.

## Events

Expand All@@ -47,28 +59,34 @@ worker.onEvent([](WorkerEvent event) {
});
```

`WorkerEvent::type` is the source of truth for event severity. Error events use `WorkerEventType::Error` and include a `WorkerStatus`.
Completion events are emitted after physical task deletion completes.

## Job Context
## Job context

Callbacks receive `WorkerJobContext&`.

| Method | Purpose |
| --- | --- |
| `id()` | Return the current job id. |
| `id()` | Return the current job ID. |
| `stop()` | Request that the current job stops. |
| `sleep(durationMs)` | Sleep the current job cooperatively. |
| `shouldStop()` | Check the cooperative stop flag. |
| `runCount()` | Number of callback runs started. |
| `startedAtMs()` | First run time from `millis()`. |
| `lastRunAtMs()` | Most recent run time from `millis()`. |

The context is valid only during callback execution.

## Diagnostics

`WorkerDiag` reports aggregate job counts and stack diagnostics. `totalJobCount`, `finishedJobCount`, `stoppedJobCount`, `failedJobCount`, stack type counts, and total stack high-water data are lifetime counters since `init()`. `runningJobCount` and `sleepingJobCount` describe currently active jobs.
`WorkerDiag` reports current state only:

- active, running, sleeping, stopping, and cleanup-queued job counts;
- cleanup-task running state;
- cleanup queue depth and high-water mark.

Completed job records are retained after they reach `Finished`, `Stopped`, or `Failed`. `WorkerJobDiag` reports state, name, stack config, run count, timing, and stack high-water data while a job is active or retained. After `waitFor()` consumes the job following task cleanup, `clearFinished()` reaps it, or Worker ends, `getJobDiagnostics()` returns `JobNotFound`.
Worker does not retain lifetime job counters. `WorkerJobDiag` is available only while a job is active. After automatic cleanup removes the active record, `getJobDiagnostics()` returns `JobNotFound`.

`waitFor()` and `stopAndWait()` return success only after the job has reached a terminal state and its task entry has completed C++ cleanup. Callback captures and task-entry RAII objects have been released before the job is reaped.
A bounded internal completion window allows `waitFor()` to observe fast jobs after their active records have already been removed. It does not retain callbacks, task handles, names, or full diagnostics.

The Worker destructor performs the same cooperative shutdown without a timeout so running tasks cannot continue after Worker internals are destroyed.
The Worker destructor performs cooperative shutdown without a timeout so tasks cannot outlive Worker internals.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 76 additions & 92 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,19 +2,21 @@

Worker is a FreeRTOS task and cooperative job execution library for ESP32.

Worker helps you run one-off and recurring background work in Arduino ESP32 projects with explicit task configuration, cooperative stop/sleep controls, event reporting, and diagnostics. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the app.
Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application.

[![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md)

## Why use Worker?

* **Task-per-job execution** - each `once()` and `every()` job owns a FreeRTOS task.
* **Safe recurring jobs** - `every()` applies the interval delay internally after each callback.
* **ESP32 task control** - configure byte stack size, priority, core affinity, and stack memory preference.
* **Cooperative lifecycle** - jobs can stop or sleep through `WorkerJobContext`.
* **Production-minded** - result-based errors, event callbacks, diagnostics, thread-safe internals, and no intentional exceptions.
- **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task.
- **Automatic cleanup** — callers never need to reap completed jobs.
- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`.
- **Safe recurring jobs** — `every()` applies the interval after each callback.
- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference.
- **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`.
- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history.

## Install

Expand All@@ -27,19 +29,17 @@ board = esp32dev
framework = arduino

lib_deps =
https://github.com/ZekStack/worker.git
https://github.com/ZekStack/worker.git

build_flags =
-std=gnu++20
-std=gnu++20
build_unflags =
-std=gnu++11
-std=gnu++11
```

### Arduino IDE

Worker is not published to Arduino Library Manager yet.

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.
Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory.

```txt
Arduino/libraries/Worker
Expand All@@ -55,60 +55,79 @@ Worker worker;
WorkerJobId recurringJob = 0;

void setup() {
Serial.begin(115200);

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
return;
}

worker.once([](WorkerJobContext &ctx) {
Serial.printf("one-off job id=%u\n", static_cast<unsigned>(ctx.id()));
});

WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {
Serial.printf("run=%u\n", static_cast<unsigned>(ctx.runCount()));
if (ctx.runCount() >= 5) {
ctx.stop();
}
});

if (result) {
recurringJob = result.jobId;
}
Serial.begin(115200);

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
return;
}

worker.once([](WorkerJobContext &ctx) {
Serial.printf("one-off job id=%u\n", static_cast<unsigned>(ctx.id()));
});

WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {
Serial.printf("run=%u\n", static_cast<unsigned>(ctx.runCount()));
if (ctx.runCount() >= 5) {
ctx.stop();
}
});

if (result) {
recurringJob = result.jobId;
}
}

void loop() {
delay(1000);
delay(1000);
}
```

No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically.

## Cleanup model

Worker creates one long-lived internal cleanup task during `init()`.

When a job callback finishes, the job:

1. releases its stored callback;
2. queues its handle and immutable allocation type;
3. suspends itself.

The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns.

This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself.

`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup.

## Important notes

> [!IMPORTANT]
> Worker cancellation is cooperative. `stop()` requests that a job stops and wakes it if it is sleeping, but it does not force-delete a running callback.

* A callback that blocks forever will prevent `stopAndWait()` and timed `end()` calls from completing before their timeout. The destructor waits without a timeout so tasks cannot outlive Worker internals.
* `every(intervalMs, callback)` delays internally after each callback.
* Completed jobs are retained until `waitFor()` can consume them after task cleanup, `clearFinished()` is called, or Worker ends.
* Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
* Custom task names are copied into a fixed internal buffer and may be truncated.
* `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
* `WorkerEvent::type` clearly identifies `Info`, `Warning`, or `Error` events.
* Worker APIs use result objects for normal failures. Catastrophic STL allocation failure while constructing result messages, callbacks, or internal containers is not recoverable by Worker on platforms where the standard library throws or aborts.
> Worker cancellation is cooperative. `stop()` requests that a job stops and wakes it if it is sleeping, but it does not interrupt a running callback.

- A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing.
- The destructor waits without a timeout so tasks cannot outlive Worker internals.
- `every(intervalMs, callback)` delays after each callback.
- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
- `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity.
- `clearFinished()` is retained only as a deprecated compatibility no-op.
- Completion synchronization uses a small bounded token window and never retains callbacks or full completed records.
- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts.

## Examples

| Example | Description |
| --- | --- |
| `Basic` | Minimal init, one-off job, recurring job, wait, and cooperative stop. |
| `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. |
| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. |
| `Events` | Event callback and error event handling. |
| `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. |
| `Diagnostics` | Aggregate diagnostics and active per-job diagnostics. |
| `Diagnostics` | Current job and cleanup-task diagnostics. |
| `BindableCallbacks` | `std::bind` with private class methods. |
| `TaskCleanupSentinel` | Runtime sentinel for verifying task-entry C++ cleanup before `waitFor()` returns. |
| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. |

Start with:

Expand All@@ -118,15 +137,13 @@ examples/Basic

## Documentation

Detailed documentation is available in the `docs/` folder.

| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Step-by-step setup and first job flow. |
| [`docs/configuration.md`](docs/configuration.md) | Config options and stack behavior. |
| [`docs/api.md`](docs/api.md) | Public classes, result types, events, and diagnostics. |
| [`docs/examples.md`](docs/examples.md) | Explanation of all included examples. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common issues and solutions. |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. |
| [`docs/examples.md`](docs/examples.md) | Example descriptions. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. |

## API overview

Expand All@@ -139,62 +156,29 @@ WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {});
WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {});

worker.sleep(loop.jobId, 5000);
worker.stopAndWait(loop.jobId, 2000);

WorkerDiag diag = worker.getDiagnostics();
WorkerJobDiag jobDiag;
worker.getJobDiagnostics(loop.jobId, jobDiag);

worker.stopAndWait(loop.jobId, 2000);
worker.clearFinished();
```

For the full API, see [`docs/api.md`](docs/api.md).

## Compatibility

| Item | Support |
| --- | --- |
| Framework | Arduino ESP32 |
| Platform | `espressif32` |
| Platform | `espressif32` / PIOArduino |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional for task stacks when ESP-IDF support is available |
| PSRAM | Optional task stacks through ESP-IDF capability APIs |
| Dependencies | none |
| Exceptions | Not used |
| Status | Early-stage `0.1.0` |

## Configuration

```cpp
WorkerConfig config;
config.defaultStackSize = 4096;
config.defaultPriority = 1;
config.defaultCoreId = tskNO_AFFINITY;
config.defaultStackType = WorkerStackType::Auto;

WorkerResult result = worker.init(config);
```

For all options, see [`docs/configuration.md`](docs/configuration.md).

## Error handling

Worker reports operation status through `WorkerResult` and `WorkerJobResult`.

```cpp
WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {});

if (!result) {
Serial.println(result.message.c_str());
return;
}
```

For result fields and status codes, see [`docs/api.md`](docs/api.md).

## License

MIT - see [`LICENSE.md`](LICENSE.md).
MIT see [`LICENSE.md`](LICENSE.md).

## ZekStack

Expand Down
60 changes: 39 additions & 21 deletions docs/api.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,36 +6,48 @@ This page summarizes the public API declared in `src/Worker.h`.

Worker does not intentionally throw exceptions. Operations report normal failures through `WorkerResult` or `WorkerJobResult`.

Catastrophic STL allocation failure while constructing result messages, callbacks, or internal containers is not recoverable by Worker on platforms where the standard library throws or aborts.

| Field | Meaning |
| --- | --- |
| `result` | `true` on success, `false` on failure. |
| `status` | Machine-readable `WorkerStatus`. |
| `message` | Human-readable status. |
| `jobId` | Returned by `WorkerJobResult` when a job was created or partially allocated. |
| `jobId` | Returned by `WorkerJobResult` after a job was created. |

`WorkerStatus` values are `Ok`, `NotInitialized`, `AlreadyInitialized`, `InvalidArgument`, `OutOfMemory`, `TaskCreateFailed`, `JobNotFound`, `Busy`, `Timeout`, and `InternalError`.

## Worker

| Method | Purpose |
| --- | --- |
| `init(config)` | Initialize Worker defaults. |
| `init(config)` | Initialize Worker and its cleanup task. |
| `onEvent(callback)` | Register a synchronous event callback. |
| `once(callback)` | Start a one-off task. |
| `once(config, callback)` | Start a configured one-off task. |
| `once(callback)` | Start an automatically cleaned one-off task. |
| `once(config, callback)` | Start a configured automatically cleaned one-off task. |
| `every(intervalMs, callback)` | Start a recurring task with internal delay. |
| `every(intervalMs, config, callback)` | Start a configured recurring task. |
| `stop(jobId)` | Request cooperative stop. |
| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until terminal state and task cleanup. |
| `stop(jobId)` | Request cooperative stop. Cleanup continues automatically. |
| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until the task stack and TCB are released. |
| `sleep(jobId, durationMs)` | Request that a job sleeps. |
| `waitFor(jobId)` | Wait until a registered or retained job reaches terminal state and task cleanup. |
| `waitFor(jobId, timeoutMs)` | Wait with timeout until terminal state and task cleanup. |
| `clearFinished()` | Reap retained terminal job records. |
| `getDiagnostics()` | Return aggregate lifetime diagnostics and current active counts. |
| `getJobDiagnostics(jobId, out)` | Fill per-job diagnostics for an active or retained terminal job. |
| `end(timeoutMs)` | Stop jobs and end Worker, returning `Timeout` if callbacks do not finish in time. |
| `waitFor(jobId)` | Optionally wait until physical task cleanup completes. |
| `waitFor(jobId, timeoutMs)` | Wait with timeout until physical task cleanup completes. |
| `clearFinished()` | Deprecated compatibility no-op. Worker cleans jobs automatically. |
| `getDiagnostics()` | Return current runtime and cleanup-task state. |
| `getJobDiagnostics(jobId, out)` | Fill diagnostics for a currently active job. |
| `end(timeoutMs)` | Stop jobs, drain cleanup, and stop Worker infrastructure. |

`once()` and `every()` are safe for fire-and-forget use. A caller never needs `waitFor()` or `clearFinished()` to release Worker-owned resources.

## Cleanup lifecycle

Worker owns every task it creates. A completed job follows this lifecycle:

1. The callback returns and its stored `std::function` is released.
2. The job queues its task handle to the Worker cleanup task.
3. The job task suspends itself.
4. The cleanup task deletes it externally with `vTaskDelete()` or `vTaskDeleteWithCaps()` as appropriate.
5. Worker records a small bounded completion token and removes the full active job record.

`waitFor()` and `stopAndWait()` succeed only after step 4. They are synchronization APIs, not cleanup APIs.

## Events

Expand All@@ -47,28 +59,34 @@ worker.onEvent([](WorkerEvent event) {
});
```

`WorkerEvent::type` is the source of truth for event severity. Error events use `WorkerEventType::Error` and include a `WorkerStatus`.
Completion events are emitted after physical task deletion completes.

## Job Context
## Job context

Callbacks receive `WorkerJobContext&`.

| Method | Purpose |
| --- | --- |
| `id()` | Return the current job id. |
| `id()` | Return the current job ID. |
| `stop()` | Request that the current job stops. |
| `sleep(durationMs)` | Sleep the current job cooperatively. |
| `shouldStop()` | Check the cooperative stop flag. |
| `runCount()` | Number of callback runs started. |
| `startedAtMs()` | First run time from `millis()`. |
| `lastRunAtMs()` | Most recent run time from `millis()`. |

The context is valid only during callback execution.

## Diagnostics

`WorkerDiag` reports aggregate job counts and stack diagnostics. `totalJobCount`, `finishedJobCount`, `stoppedJobCount`, `failedJobCount`, stack type counts, and total stack high-water data are lifetime counters since `init()`. `runningJobCount` and `sleepingJobCount` describe currently active jobs.
`WorkerDiag` reports current state only:

- active, running, sleeping, stopping, and cleanup-queued job counts;
- cleanup-task running state;
- cleanup queue depth and high-water mark.

Completed job records are retained after they reach `Finished`, `Stopped`, or `Failed`. `WorkerJobDiag` reports state, name, stack config, run count, timing, and stack high-water data while a job is active or retained. After `waitFor()` consumes the job following task cleanup, `clearFinished()` reaps it, or Worker ends, `getJobDiagnostics()` returns `JobNotFound`.
Worker does not retain lifetime job counters. `WorkerJobDiag` is available only while a job is active. After automatic cleanup removes the active record, `getJobDiagnostics()` returns `JobNotFound`.

`waitFor()` and `stopAndWait()` return success only after the job has reached a terminal state and its task entry has completed C++ cleanup. Callback captures and task-entry RAII objects have been released before the job is reaped.
A bounded internal completion window allows `waitFor()` to observe fast jobs after their active records have already been removed. It does not retain callbacks, task handles, names, or full diagnostics.

The Worker destructor performs the same cooperative shutdown without a timeout so running tasks cannot continue after Worker internals are destroyed.
The Worker destructor performs cooperative shutdown without a timeout so tasks cannot outlive Worker internals.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 76 additions & 92 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,19 +2,21 @@

Worker is a FreeRTOS task and cooperative job execution library for ESP32.

Worker helps you run one-off and recurring background work in Arduino ESP32 projects with explicit task configuration, cooperative stop/sleep controls, event reporting, and diagnostics. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the app.
Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application.

[![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md)

## Why use Worker?

* **Task-per-job execution** - each `once()` and `every()` job owns a FreeRTOS task.
* **Safe recurring jobs** - `every()` applies the interval delay internally after each callback.
* **ESP32 task control** - configure byte stack size, priority, core affinity, and stack memory preference.
* **Cooperative lifecycle** - jobs can stop or sleep through `WorkerJobContext`.
* **Production-minded** - result-based errors, event callbacks, diagnostics, thread-safe internals, and no intentional exceptions.
- **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task.
- **Automatic cleanup** — callers never need to reap completed jobs.
- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`.
- **Safe recurring jobs** — `every()` applies the interval after each callback.
- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference.
- **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`.
- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history.

## Install

Expand All@@ -27,19 +29,17 @@ board = esp32dev
framework = arduino

lib_deps =
https://github.com/ZekStack/worker.git
https://github.com/ZekStack/worker.git

build_flags =
-std=gnu++20
-std=gnu++20
build_unflags =
-std=gnu++11
-std=gnu++11
```

### Arduino IDE

Worker is not published to Arduino Library Manager yet.

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.
Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory.

```txt
Arduino/libraries/Worker
Expand All@@ -55,60 +55,79 @@ Worker worker;
WorkerJobId recurringJob = 0;

void setup() {
Serial.begin(115200);

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
return;
}

worker.once([](WorkerJobContext &ctx) {
Serial.printf("one-off job id=%u\n", static_cast<unsigned>(ctx.id()));
});

WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {
Serial.printf("run=%u\n", static_cast<unsigned>(ctx.runCount()));
if (ctx.runCount() >= 5) {
ctx.stop();
}
});

if (result) {
recurringJob = result.jobId;
}
Serial.begin(115200);

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
return;
}

worker.once([](WorkerJobContext &ctx) {
Serial.printf("one-off job id=%u\n", static_cast<unsigned>(ctx.id()));
});

WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {
Serial.printf("run=%u\n", static_cast<unsigned>(ctx.runCount()));
if (ctx.runCount() >= 5) {
ctx.stop();
}
});

if (result) {
recurringJob = result.jobId;
}
}

void loop() {
delay(1000);
delay(1000);
}
```

No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically.

## Cleanup model

Worker creates one long-lived internal cleanup task during `init()`.

When a job callback finishes, the job:

1. releases its stored callback;
2. queues its handle and immutable allocation type;
3. suspends itself.

The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns.

This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself.

`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup.

## Important notes

> [!IMPORTANT]
> Worker cancellation is cooperative. `stop()` requests that a job stops and wakes it if it is sleeping, but it does not force-delete a running callback.

* A callback that blocks forever will prevent `stopAndWait()` and timed `end()` calls from completing before their timeout. The destructor waits without a timeout so tasks cannot outlive Worker internals.
* `every(intervalMs, callback)` delays internally after each callback.
* Completed jobs are retained until `waitFor()` can consume them after task cleanup, `clearFinished()` is called, or Worker ends.
* Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
* Custom task names are copied into a fixed internal buffer and may be truncated.
* `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
* `WorkerEvent::type` clearly identifies `Info`, `Warning`, or `Error` events.
* Worker APIs use result objects for normal failures. Catastrophic STL allocation failure while constructing result messages, callbacks, or internal containers is not recoverable by Worker on platforms where the standard library throws or aborts.
> Worker cancellation is cooperative. `stop()` requests that a job stops and wakes it if it is sleeping, but it does not interrupt a running callback.

- A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing.
- The destructor waits without a timeout so tasks cannot outlive Worker internals.
- `every(intervalMs, callback)` delays after each callback.
- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
- `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity.
- `clearFinished()` is retained only as a deprecated compatibility no-op.
- Completion synchronization uses a small bounded token window and never retains callbacks or full completed records.
- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts.

## Examples

| Example | Description |
| --- | --- |
| `Basic` | Minimal init, one-off job, recurring job, wait, and cooperative stop. |
| `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. |
| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. |
| `Events` | Event callback and error event handling. |
| `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. |
| `Diagnostics` | Aggregate diagnostics and active per-job diagnostics. |
| `Diagnostics` | Current job and cleanup-task diagnostics. |
| `BindableCallbacks` | `std::bind` with private class methods. |
| `TaskCleanupSentinel` | Runtime sentinel for verifying task-entry C++ cleanup before `waitFor()` returns. |
| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. |

Start with:

Expand All@@ -118,15 +137,13 @@ examples/Basic

## Documentation

Detailed documentation is available in the `docs/` folder.

| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Step-by-step setup and first job flow. |
| [`docs/configuration.md`](docs/configuration.md) | Config options and stack behavior. |
| [`docs/api.md`](docs/api.md) | Public classes, result types, events, and diagnostics. |
| [`docs/examples.md`](docs/examples.md) | Explanation of all included examples. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common issues and solutions. |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. |
| [`docs/examples.md`](docs/examples.md) | Example descriptions. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. |

## API overview

Expand All@@ -139,62 +156,29 @@ WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {});
WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {});

worker.sleep(loop.jobId, 5000);
worker.stopAndWait(loop.jobId, 2000);

WorkerDiag diag = worker.getDiagnostics();
WorkerJobDiag jobDiag;
worker.getJobDiagnostics(loop.jobId, jobDiag);

worker.stopAndWait(loop.jobId, 2000);
worker.clearFinished();
```

For the full API, see [`docs/api.md`](docs/api.md).

## Compatibility

| Item | Support |
| --- | --- |
| Framework | Arduino ESP32 |
| Platform | `espressif32` |
| Platform | `espressif32` / PIOArduino |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional for task stacks when ESP-IDF support is available |
| PSRAM | Optional task stacks through ESP-IDF capability APIs |
| Dependencies | none |
| Exceptions | Not used |
| Status | Early-stage `0.1.0` |

## Configuration

```cpp
WorkerConfig config;
config.defaultStackSize = 4096;
config.defaultPriority = 1;
config.defaultCoreId = tskNO_AFFINITY;
config.defaultStackType = WorkerStackType::Auto;

WorkerResult result = worker.init(config);
```

For all options, see [`docs/configuration.md`](docs/configuration.md).

## Error handling

Worker reports operation status through `WorkerResult` and `WorkerJobResult`.

```cpp
WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {});

if (!result) {
Serial.println(result.message.c_str());
return;
}
```

For result fields and status codes, see [`docs/api.md`](docs/api.md).

## License

MIT - see [`LICENSE.md`](LICENSE.md).
MIT see [`LICENSE.md`](LICENSE.md).

## ZekStack

Expand Down
60 changes: 39 additions & 21 deletions docs/api.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,36 +6,48 @@ This page summarizes the public API declared in `src/Worker.h`.

Worker does not intentionally throw exceptions. Operations report normal failures through `WorkerResult` or `WorkerJobResult`.

Catastrophic STL allocation failure while constructing result messages, callbacks, or internal containers is not recoverable by Worker on platforms where the standard library throws or aborts.

| Field | Meaning |
| --- | --- |
| `result` | `true` on success, `false` on failure. |
| `status` | Machine-readable `WorkerStatus`. |
| `message` | Human-readable status. |
| `jobId` | Returned by `WorkerJobResult` when a job was created or partially allocated. |
| `jobId` | Returned by `WorkerJobResult` after a job was created. |

`WorkerStatus` values are `Ok`, `NotInitialized`, `AlreadyInitialized`, `InvalidArgument`, `OutOfMemory`, `TaskCreateFailed`, `JobNotFound`, `Busy`, `Timeout`, and `InternalError`.

## Worker

| Method | Purpose |
| --- | --- |
| `init(config)` | Initialize Worker defaults. |
| `init(config)` | Initialize Worker and its cleanup task. |
| `onEvent(callback)` | Register a synchronous event callback. |
| `once(callback)` | Start a one-off task. |
| `once(config, callback)` | Start a configured one-off task. |
| `once(callback)` | Start an automatically cleaned one-off task. |
| `once(config, callback)` | Start a configured automatically cleaned one-off task. |
| `every(intervalMs, callback)` | Start a recurring task with internal delay. |
| `every(intervalMs, config, callback)` | Start a configured recurring task. |
| `stop(jobId)` | Request cooperative stop. |
| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until terminal state and task cleanup. |
| `stop(jobId)` | Request cooperative stop. Cleanup continues automatically. |
| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until the task stack and TCB are released. |
| `sleep(jobId, durationMs)` | Request that a job sleeps. |
| `waitFor(jobId)` | Wait until a registered or retained job reaches terminal state and task cleanup. |
| `waitFor(jobId, timeoutMs)` | Wait with timeout until terminal state and task cleanup. |
| `clearFinished()` | Reap retained terminal job records. |
| `getDiagnostics()` | Return aggregate lifetime diagnostics and current active counts. |
| `getJobDiagnostics(jobId, out)` | Fill per-job diagnostics for an active or retained terminal job. |
| `end(timeoutMs)` | Stop jobs and end Worker, returning `Timeout` if callbacks do not finish in time. |
| `waitFor(jobId)` | Optionally wait until physical task cleanup completes. |
| `waitFor(jobId, timeoutMs)` | Wait with timeout until physical task cleanup completes. |
| `clearFinished()` | Deprecated compatibility no-op. Worker cleans jobs automatically. |
| `getDiagnostics()` | Return current runtime and cleanup-task state. |
| `getJobDiagnostics(jobId, out)` | Fill diagnostics for a currently active job. |
| `end(timeoutMs)` | Stop jobs, drain cleanup, and stop Worker infrastructure. |

`once()` and `every()` are safe for fire-and-forget use. A caller never needs `waitFor()` or `clearFinished()` to release Worker-owned resources.

## Cleanup lifecycle

Worker owns every task it creates. A completed job follows this lifecycle:

1. The callback returns and its stored `std::function` is released.
2. The job queues its task handle to the Worker cleanup task.
3. The job task suspends itself.
4. The cleanup task deletes it externally with `vTaskDelete()` or `vTaskDeleteWithCaps()` as appropriate.
5. Worker records a small bounded completion token and removes the full active job record.

`waitFor()` and `stopAndWait()` succeed only after step 4. They are synchronization APIs, not cleanup APIs.

## Events

Expand All@@ -47,28 +59,34 @@ worker.onEvent([](WorkerEvent event) {
});
```

`WorkerEvent::type` is the source of truth for event severity. Error events use `WorkerEventType::Error` and include a `WorkerStatus`.
Completion events are emitted after physical task deletion completes.

## Job Context
## Job context

Callbacks receive `WorkerJobContext&`.

| Method | Purpose |
| --- | --- |
| `id()` | Return the current job id. |
| `id()` | Return the current job ID. |
| `stop()` | Request that the current job stops. |
| `sleep(durationMs)` | Sleep the current job cooperatively. |
| `shouldStop()` | Check the cooperative stop flag. |
| `runCount()` | Number of callback runs started. |
| `startedAtMs()` | First run time from `millis()`. |
| `lastRunAtMs()` | Most recent run time from `millis()`. |

The context is valid only during callback execution.

## Diagnostics

`WorkerDiag` reports aggregate job counts and stack diagnostics. `totalJobCount`, `finishedJobCount`, `stoppedJobCount`, `failedJobCount`, stack type counts, and total stack high-water data are lifetime counters since `init()`. `runningJobCount` and `sleepingJobCount` describe currently active jobs.
`WorkerDiag` reports current state only:

- active, running, sleeping, stopping, and cleanup-queued job counts;
- cleanup-task running state;
- cleanup queue depth and high-water mark.

Completed job records are retained after they reach `Finished`, `Stopped`, or `Failed`. `WorkerJobDiag` reports state, name, stack config, run count, timing, and stack high-water data while a job is active or retained. After `waitFor()` consumes the job following task cleanup, `clearFinished()` reaps it, or Worker ends, `getJobDiagnostics()` returns `JobNotFound`.
Worker does not retain lifetime job counters. `WorkerJobDiag` is available only while a job is active. After automatic cleanup removes the active record, `getJobDiagnostics()` returns `JobNotFound`.

`waitFor()` and `stopAndWait()` return success only after the job has reached a terminal state and its task entry has completed C++ cleanup. Callback captures and task-entry RAII objects have been released before the job is reaped.
A bounded internal completion window allows `waitFor()` to observe fast jobs after their active records have already been removed. It does not retain callbacks, task handles, names, or full diagnostics.

The Worker destructor performs the same cooperative shutdown without a timeout so running tasks cannot continue after Worker internals are destroyed.
The Worker destructor performs cooperative shutdown without a timeout so tasks cannot outlive Worker internals.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 76 additions & 92 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,19 +2,21 @@

Worker is a FreeRTOS task and cooperative job execution library for ESP32.

Worker helps you run one-off and recurring background work in Arduino ESP32 projects with explicit task configuration, cooperative stop/sleep controls, event reporting, and diagnostics. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the app.
Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application.

[![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md)

## Why use Worker?

* **Task-per-job execution** - each `once()` and `every()` job owns a FreeRTOS task.
* **Safe recurring jobs** - `every()` applies the interval delay internally after each callback.
* **ESP32 task control** - configure byte stack size, priority, core affinity, and stack memory preference.
* **Cooperative lifecycle** - jobs can stop or sleep through `WorkerJobContext`.
* **Production-minded** - result-based errors, event callbacks, diagnostics, thread-safe internals, and no intentional exceptions.
- **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task.
- **Automatic cleanup** — callers never need to reap completed jobs.
- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`.
- **Safe recurring jobs** — `every()` applies the interval after each callback.
- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference.
- **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`.
- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history.

## Install

Expand All@@ -27,19 +29,17 @@ board = esp32dev
framework = arduino

lib_deps =
https://github.com/ZekStack/worker.git
https://github.com/ZekStack/worker.git

build_flags =
-std=gnu++20
-std=gnu++20
build_unflags =
-std=gnu++11
-std=gnu++11
```

### Arduino IDE

Worker is not published to Arduino Library Manager yet.

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.
Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory.

```txt
Arduino/libraries/Worker
Expand All@@ -55,60 +55,79 @@ Worker worker;
WorkerJobId recurringJob = 0;

void setup() {
Serial.begin(115200);

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
return;
}

worker.once([](WorkerJobContext &ctx) {
Serial.printf("one-off job id=%u\n", static_cast<unsigned>(ctx.id()));
});

WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {
Serial.printf("run=%u\n", static_cast<unsigned>(ctx.runCount()));
if (ctx.runCount() >= 5) {
ctx.stop();
}
});

if (result) {
recurringJob = result.jobId;
}
Serial.begin(115200);

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
return;
}

worker.once([](WorkerJobContext &ctx) {
Serial.printf("one-off job id=%u\n", static_cast<unsigned>(ctx.id()));
});

WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {
Serial.printf("run=%u\n", static_cast<unsigned>(ctx.runCount()));
if (ctx.runCount() >= 5) {
ctx.stop();
}
});

if (result) {
recurringJob = result.jobId;
}
}

void loop() {
delay(1000);
delay(1000);
}
```

No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically.

## Cleanup model

Worker creates one long-lived internal cleanup task during `init()`.

When a job callback finishes, the job:

1. releases its stored callback;
2. queues its handle and immutable allocation type;
3. suspends itself.

The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns.

This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself.

`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup.

## Important notes

> [!IMPORTANT]
> Worker cancellation is cooperative. `stop()` requests that a job stops and wakes it if it is sleeping, but it does not force-delete a running callback.

* A callback that blocks forever will prevent `stopAndWait()` and timed `end()` calls from completing before their timeout. The destructor waits without a timeout so tasks cannot outlive Worker internals.
* `every(intervalMs, callback)` delays internally after each callback.
* Completed jobs are retained until `waitFor()` can consume them after task cleanup, `clearFinished()` is called, or Worker ends.
* Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
* Custom task names are copied into a fixed internal buffer and may be truncated.
* `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
* `WorkerEvent::type` clearly identifies `Info`, `Warning`, or `Error` events.
* Worker APIs use result objects for normal failures. Catastrophic STL allocation failure while constructing result messages, callbacks, or internal containers is not recoverable by Worker on platforms where the standard library throws or aborts.
> Worker cancellation is cooperative. `stop()` requests that a job stops and wakes it if it is sleeping, but it does not interrupt a running callback.

- A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing.
- The destructor waits without a timeout so tasks cannot outlive Worker internals.
- `every(intervalMs, callback)` delays after each callback.
- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
- `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity.
- `clearFinished()` is retained only as a deprecated compatibility no-op.
- Completion synchronization uses a small bounded token window and never retains callbacks or full completed records.
- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts.

## Examples

| Example | Description |
| --- | --- |
| `Basic` | Minimal init, one-off job, recurring job, wait, and cooperative stop. |
| `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. |
| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. |
| `Events` | Event callback and error event handling. |
| `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. |
| `Diagnostics` | Aggregate diagnostics and active per-job diagnostics. |
| `Diagnostics` | Current job and cleanup-task diagnostics. |
| `BindableCallbacks` | `std::bind` with private class methods. |
| `TaskCleanupSentinel` | Runtime sentinel for verifying task-entry C++ cleanup before `waitFor()` returns. |
| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. |

Start with:

Expand All@@ -118,15 +137,13 @@ examples/Basic

## Documentation

Detailed documentation is available in the `docs/` folder.

| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Step-by-step setup and first job flow. |
| [`docs/configuration.md`](docs/configuration.md) | Config options and stack behavior. |
| [`docs/api.md`](docs/api.md) | Public classes, result types, events, and diagnostics. |
| [`docs/examples.md`](docs/examples.md) | Explanation of all included examples. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common issues and solutions. |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. |
| [`docs/examples.md`](docs/examples.md) | Example descriptions. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. |

## API overview

Expand All@@ -139,62 +156,29 @@ WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {});
WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {});

worker.sleep(loop.jobId, 5000);
worker.stopAndWait(loop.jobId, 2000);

WorkerDiag diag = worker.getDiagnostics();
WorkerJobDiag jobDiag;
worker.getJobDiagnostics(loop.jobId, jobDiag);

worker.stopAndWait(loop.jobId, 2000);
worker.clearFinished();
```

For the full API, see [`docs/api.md`](docs/api.md).

## Compatibility

| Item | Support |
| --- | --- |
| Framework | Arduino ESP32 |
| Platform | `espressif32` |
| Platform | `espressif32` / PIOArduino |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional for task stacks when ESP-IDF support is available |
| PSRAM | Optional task stacks through ESP-IDF capability APIs |
| Dependencies | none |
| Exceptions | Not used |
| Status | Early-stage `0.1.0` |

## Configuration

```cpp
WorkerConfig config;
config.defaultStackSize = 4096;
config.defaultPriority = 1;
config.defaultCoreId = tskNO_AFFINITY;
config.defaultStackType = WorkerStackType::Auto;

WorkerResult result = worker.init(config);
```

For all options, see [`docs/configuration.md`](docs/configuration.md).

## Error handling

Worker reports operation status through `WorkerResult` and `WorkerJobResult`.

```cpp
WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {});

if (!result) {
Serial.println(result.message.c_str());
return;
}
```

For result fields and status codes, see [`docs/api.md`](docs/api.md).

## License

MIT - see [`LICENSE.md`](LICENSE.md).
MIT see [`LICENSE.md`](LICENSE.md).

## ZekStack

Expand Down
60 changes: 39 additions & 21 deletions docs/api.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,36 +6,48 @@ This page summarizes the public API declared in `src/Worker.h`.

Worker does not intentionally throw exceptions. Operations report normal failures through `WorkerResult` or `WorkerJobResult`.

Catastrophic STL allocation failure while constructing result messages, callbacks, or internal containers is not recoverable by Worker on platforms where the standard library throws or aborts.

| Field | Meaning |
| --- | --- |
| `result` | `true` on success, `false` on failure. |
| `status` | Machine-readable `WorkerStatus`. |
| `message` | Human-readable status. |
| `jobId` | Returned by `WorkerJobResult` when a job was created or partially allocated. |
| `jobId` | Returned by `WorkerJobResult` after a job was created. |

`WorkerStatus` values are `Ok`, `NotInitialized`, `AlreadyInitialized`, `InvalidArgument`, `OutOfMemory`, `TaskCreateFailed`, `JobNotFound`, `Busy`, `Timeout`, and `InternalError`.

## Worker

| Method | Purpose |
| --- | --- |
| `init(config)` | Initialize Worker defaults. |
| `init(config)` | Initialize Worker and its cleanup task. |
| `onEvent(callback)` | Register a synchronous event callback. |
| `once(callback)` | Start a one-off task. |
| `once(config, callback)` | Start a configured one-off task. |
| `once(callback)` | Start an automatically cleaned one-off task. |
| `once(config, callback)` | Start a configured automatically cleaned one-off task. |
| `every(intervalMs, callback)` | Start a recurring task with internal delay. |
| `every(intervalMs, config, callback)` | Start a configured recurring task. |
| `stop(jobId)` | Request cooperative stop. |
| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until terminal state and task cleanup. |
| `stop(jobId)` | Request cooperative stop. Cleanup continues automatically. |
| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until the task stack and TCB are released. |
| `sleep(jobId, durationMs)` | Request that a job sleeps. |
| `waitFor(jobId)` | Wait until a registered or retained job reaches terminal state and task cleanup. |
| `waitFor(jobId, timeoutMs)` | Wait with timeout until terminal state and task cleanup. |
| `clearFinished()` | Reap retained terminal job records. |
| `getDiagnostics()` | Return aggregate lifetime diagnostics and current active counts. |
| `getJobDiagnostics(jobId, out)` | Fill per-job diagnostics for an active or retained terminal job. |
| `end(timeoutMs)` | Stop jobs and end Worker, returning `Timeout` if callbacks do not finish in time. |
| `waitFor(jobId)` | Optionally wait until physical task cleanup completes. |
| `waitFor(jobId, timeoutMs)` | Wait with timeout until physical task cleanup completes. |
| `clearFinished()` | Deprecated compatibility no-op. Worker cleans jobs automatically. |
| `getDiagnostics()` | Return current runtime and cleanup-task state. |
| `getJobDiagnostics(jobId, out)` | Fill diagnostics for a currently active job. |
| `end(timeoutMs)` | Stop jobs, drain cleanup, and stop Worker infrastructure. |

`once()` and `every()` are safe for fire-and-forget use. A caller never needs `waitFor()` or `clearFinished()` to release Worker-owned resources.

## Cleanup lifecycle

Worker owns every task it creates. A completed job follows this lifecycle:

1. The callback returns and its stored `std::function` is released.
2. The job queues its task handle to the Worker cleanup task.
3. The job task suspends itself.
4. The cleanup task deletes it externally with `vTaskDelete()` or `vTaskDeleteWithCaps()` as appropriate.
5. Worker records a small bounded completion token and removes the full active job record.

`waitFor()` and `stopAndWait()` succeed only after step 4. They are synchronization APIs, not cleanup APIs.

## Events

Expand All@@ -47,28 +59,34 @@ worker.onEvent([](WorkerEvent event) {
});
```

`WorkerEvent::type` is the source of truth for event severity. Error events use `WorkerEventType::Error` and include a `WorkerStatus`.
Completion events are emitted after physical task deletion completes.

## Job Context
## Job context

Callbacks receive `WorkerJobContext&`.

| Method | Purpose |
| --- | --- |
| `id()` | Return the current job id. |
| `id()` | Return the current job ID. |
| `stop()` | Request that the current job stops. |
| `sleep(durationMs)` | Sleep the current job cooperatively. |
| `shouldStop()` | Check the cooperative stop flag. |
| `runCount()` | Number of callback runs started. |
| `startedAtMs()` | First run time from `millis()`. |
| `lastRunAtMs()` | Most recent run time from `millis()`. |

The context is valid only during callback execution.

## Diagnostics

`WorkerDiag` reports aggregate job counts and stack diagnostics. `totalJobCount`, `finishedJobCount`, `stoppedJobCount`, `failedJobCount`, stack type counts, and total stack high-water data are lifetime counters since `init()`. `runningJobCount` and `sleepingJobCount` describe currently active jobs.
`WorkerDiag` reports current state only:

- active, running, sleeping, stopping, and cleanup-queued job counts;
- cleanup-task running state;
- cleanup queue depth and high-water mark.

Completed job records are retained after they reach `Finished`, `Stopped`, or `Failed`. `WorkerJobDiag` reports state, name, stack config, run count, timing, and stack high-water data while a job is active or retained. After `waitFor()` consumes the job following task cleanup, `clearFinished()` reaps it, or Worker ends, `getJobDiagnostics()` returns `JobNotFound`.
Worker does not retain lifetime job counters. `WorkerJobDiag` is available only while a job is active. After automatic cleanup removes the active record, `getJobDiagnostics()` returns `JobNotFound`.

`waitFor()` and `stopAndWait()` return success only after the job has reached a terminal state and its task entry has completed C++ cleanup. Callback captures and task-entry RAII objects have been released before the job is reaped.
A bounded internal completion window allows `waitFor()` to observe fast jobs after their active records have already been removed. It does not retain callbacks, task handles, names, or full diagnostics.

The Worker destructor performs the same cooperative shutdown without a timeout so running tasks cannot continue after Worker internals are destroyed.
The Worker destructor performs cooperative shutdown without a timeout so tasks cannot outlive Worker internals.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 76 additions & 92 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,19 +2,21 @@

Worker is a FreeRTOS task and cooperative job execution library for ESP32.

Worker helps you run one-off and recurring background work in Arduino ESP32 projects with explicit task configuration, cooperative stop/sleep controls, event reporting, and diagnostics. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the app.
Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application.

[![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md)

## Why use Worker?

* **Task-per-job execution** - each `once()` and `every()` job owns a FreeRTOS task.
* **Safe recurring jobs** - `every()` applies the interval delay internally after each callback.
* **ESP32 task control** - configure byte stack size, priority, core affinity, and stack memory preference.
* **Cooperative lifecycle** - jobs can stop or sleep through `WorkerJobContext`.
* **Production-minded** - result-based errors, event callbacks, diagnostics, thread-safe internals, and no intentional exceptions.
- **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task.
- **Automatic cleanup** — callers never need to reap completed jobs.
- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`.
- **Safe recurring jobs** — `every()` applies the interval after each callback.
- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference.
- **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`.
- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history.

## Install

Expand All@@ -27,19 +29,17 @@ board = esp32dev
framework = arduino

lib_deps =
https://github.com/ZekStack/worker.git
https://github.com/ZekStack/worker.git

build_flags =
-std=gnu++20
-std=gnu++20
build_unflags =
-std=gnu++11
-std=gnu++11
```

### Arduino IDE

Worker is not published to Arduino Library Manager yet.

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.
Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory.

```txt
Arduino/libraries/Worker
Expand All@@ -55,60 +55,79 @@ Worker worker;
WorkerJobId recurringJob = 0;

void setup() {
Serial.begin(115200);

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
return;
}

worker.once([](WorkerJobContext &ctx) {
Serial.printf("one-off job id=%u\n", static_cast<unsigned>(ctx.id()));
});

WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {
Serial.printf("run=%u\n", static_cast<unsigned>(ctx.runCount()));
if (ctx.runCount() >= 5) {
ctx.stop();
}
});

if (result) {
recurringJob = result.jobId;
}
Serial.begin(115200);

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
return;
}

worker.once([](WorkerJobContext &ctx) {
Serial.printf("one-off job id=%u\n", static_cast<unsigned>(ctx.id()));
});

WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {
Serial.printf("run=%u\n", static_cast<unsigned>(ctx.runCount()));
if (ctx.runCount() >= 5) {
ctx.stop();
}
});

if (result) {
recurringJob = result.jobId;
}
}

void loop() {
delay(1000);
delay(1000);
}
```

No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically.

## Cleanup model

Worker creates one long-lived internal cleanup task during `init()`.

When a job callback finishes, the job:

1. releases its stored callback;
2. queues its handle and immutable allocation type;
3. suspends itself.

The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns.

This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself.

`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup.

## Important notes

> [!IMPORTANT]
> Worker cancellation is cooperative. `stop()` requests that a job stops and wakes it if it is sleeping, but it does not force-delete a running callback.

* A callback that blocks forever will prevent `stopAndWait()` and timed `end()` calls from completing before their timeout. The destructor waits without a timeout so tasks cannot outlive Worker internals.
* `every(intervalMs, callback)` delays internally after each callback.
* Completed jobs are retained until `waitFor()` can consume them after task cleanup, `clearFinished()` is called, or Worker ends.
* Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
* Custom task names are copied into a fixed internal buffer and may be truncated.
* `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
* `WorkerEvent::type` clearly identifies `Info`, `Warning`, or `Error` events.
* Worker APIs use result objects for normal failures. Catastrophic STL allocation failure while constructing result messages, callbacks, or internal containers is not recoverable by Worker on platforms where the standard library throws or aborts.
> Worker cancellation is cooperative. `stop()` requests that a job stops and wakes it if it is sleeping, but it does not interrupt a running callback.

- A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing.
- The destructor waits without a timeout so tasks cannot outlive Worker internals.
- `every(intervalMs, callback)` delays after each callback.
- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
- `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity.
- `clearFinished()` is retained only as a deprecated compatibility no-op.
- Completion synchronization uses a small bounded token window and never retains callbacks or full completed records.
- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts.

## Examples

| Example | Description |
| --- | --- |
| `Basic` | Minimal init, one-off job, recurring job, wait, and cooperative stop. |
| `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. |
| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. |
| `Events` | Event callback and error event handling. |
| `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. |
| `Diagnostics` | Aggregate diagnostics and active per-job diagnostics. |
| `Diagnostics` | Current job and cleanup-task diagnostics. |
| `BindableCallbacks` | `std::bind` with private class methods. |
| `TaskCleanupSentinel` | Runtime sentinel for verifying task-entry C++ cleanup before `waitFor()` returns. |
| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. |

Start with:

Expand All@@ -118,15 +137,13 @@ examples/Basic

## Documentation

Detailed documentation is available in the `docs/` folder.

| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Step-by-step setup and first job flow. |
| [`docs/configuration.md`](docs/configuration.md) | Config options and stack behavior. |
| [`docs/api.md`](docs/api.md) | Public classes, result types, events, and diagnostics. |
| [`docs/examples.md`](docs/examples.md) | Explanation of all included examples. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common issues and solutions. |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. |
| [`docs/examples.md`](docs/examples.md) | Example descriptions. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. |

## API overview

Expand All@@ -139,62 +156,29 @@ WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {});
WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {});

worker.sleep(loop.jobId, 5000);
worker.stopAndWait(loop.jobId, 2000);

WorkerDiag diag = worker.getDiagnostics();
WorkerJobDiag jobDiag;
worker.getJobDiagnostics(loop.jobId, jobDiag);

worker.stopAndWait(loop.jobId, 2000);
worker.clearFinished();
```

For the full API, see [`docs/api.md`](docs/api.md).

## Compatibility

| Item | Support |
| --- | --- |
| Framework | Arduino ESP32 |
| Platform | `espressif32` |
| Platform | `espressif32` / PIOArduino |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional for task stacks when ESP-IDF support is available |
| PSRAM | Optional task stacks through ESP-IDF capability APIs |
| Dependencies | none |
| Exceptions | Not used |
| Status | Early-stage `0.1.0` |

## Configuration

```cpp
WorkerConfig config;
config.defaultStackSize = 4096;
config.defaultPriority = 1;
config.defaultCoreId = tskNO_AFFINITY;
config.defaultStackType = WorkerStackType::Auto;

WorkerResult result = worker.init(config);
```

For all options, see [`docs/configuration.md`](docs/configuration.md).

## Error handling

Worker reports operation status through `WorkerResult` and `WorkerJobResult`.

```cpp
WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {});

if (!result) {
Serial.println(result.message.c_str());
return;
}
```

For result fields and status codes, see [`docs/api.md`](docs/api.md).

## License

MIT - see [`LICENSE.md`](LICENSE.md).
MIT see [`LICENSE.md`](LICENSE.md).

## ZekStack

Expand Down
60 changes: 39 additions & 21 deletions docs/api.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,36 +6,48 @@ This page summarizes the public API declared in `src/Worker.h`.

Worker does not intentionally throw exceptions. Operations report normal failures through `WorkerResult` or `WorkerJobResult`.

Catastrophic STL allocation failure while constructing result messages, callbacks, or internal containers is not recoverable by Worker on platforms where the standard library throws or aborts.

| Field | Meaning |
| --- | --- |
| `result` | `true` on success, `false` on failure. |
| `status` | Machine-readable `WorkerStatus`. |
| `message` | Human-readable status. |
| `jobId` | Returned by `WorkerJobResult` when a job was created or partially allocated. |
| `jobId` | Returned by `WorkerJobResult` after a job was created. |

`WorkerStatus` values are `Ok`, `NotInitialized`, `AlreadyInitialized`, `InvalidArgument`, `OutOfMemory`, `TaskCreateFailed`, `JobNotFound`, `Busy`, `Timeout`, and `InternalError`.

## Worker

| Method | Purpose |
| --- | --- |
| `init(config)` | Initialize Worker defaults. |
| `init(config)` | Initialize Worker and its cleanup task. |
| `onEvent(callback)` | Register a synchronous event callback. |
| `once(callback)` | Start a one-off task. |
| `once(config, callback)` | Start a configured one-off task. |
| `once(callback)` | Start an automatically cleaned one-off task. |
| `once(config, callback)` | Start a configured automatically cleaned one-off task. |
| `every(intervalMs, callback)` | Start a recurring task with internal delay. |
| `every(intervalMs, config, callback)` | Start a configured recurring task. |
| `stop(jobId)` | Request cooperative stop. |
| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until terminal state and task cleanup. |
| `stop(jobId)` | Request cooperative stop. Cleanup continues automatically. |
| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until the task stack and TCB are released. |
| `sleep(jobId, durationMs)` | Request that a job sleeps. |
| `waitFor(jobId)` | Wait until a registered or retained job reaches terminal state and task cleanup. |
| `waitFor(jobId, timeoutMs)` | Wait with timeout until terminal state and task cleanup. |
| `clearFinished()` | Reap retained terminal job records. |
| `getDiagnostics()` | Return aggregate lifetime diagnostics and current active counts. |
| `getJobDiagnostics(jobId, out)` | Fill per-job diagnostics for an active or retained terminal job. |
| `end(timeoutMs)` | Stop jobs and end Worker, returning `Timeout` if callbacks do not finish in time. |
| `waitFor(jobId)` | Optionally wait until physical task cleanup completes. |
| `waitFor(jobId, timeoutMs)` | Wait with timeout until physical task cleanup completes. |
| `clearFinished()` | Deprecated compatibility no-op. Worker cleans jobs automatically. |
| `getDiagnostics()` | Return current runtime and cleanup-task state. |
| `getJobDiagnostics(jobId, out)` | Fill diagnostics for a currently active job. |
| `end(timeoutMs)` | Stop jobs, drain cleanup, and stop Worker infrastructure. |

`once()` and `every()` are safe for fire-and-forget use. A caller never needs `waitFor()` or `clearFinished()` to release Worker-owned resources.

## Cleanup lifecycle

Worker owns every task it creates. A completed job follows this lifecycle:

1. The callback returns and its stored `std::function` is released.
2. The job queues its task handle to the Worker cleanup task.
3. The job task suspends itself.
4. The cleanup task deletes it externally with `vTaskDelete()` or `vTaskDeleteWithCaps()` as appropriate.
5. Worker records a small bounded completion token and removes the full active job record.

`waitFor()` and `stopAndWait()` succeed only after step 4. They are synchronization APIs, not cleanup APIs.

## Events

Expand All@@ -47,28 +59,34 @@ worker.onEvent([](WorkerEvent event) {
});
```

`WorkerEvent::type` is the source of truth for event severity. Error events use `WorkerEventType::Error` and include a `WorkerStatus`.
Completion events are emitted after physical task deletion completes.

## Job Context
## Job context

Callbacks receive `WorkerJobContext&`.

| Method | Purpose |
| --- | --- |
| `id()` | Return the current job id. |
| `id()` | Return the current job ID. |
| `stop()` | Request that the current job stops. |
| `sleep(durationMs)` | Sleep the current job cooperatively. |
| `shouldStop()` | Check the cooperative stop flag. |
| `runCount()` | Number of callback runs started. |
| `startedAtMs()` | First run time from `millis()`. |
| `lastRunAtMs()` | Most recent run time from `millis()`. |

The context is valid only during callback execution.

## Diagnostics

`WorkerDiag` reports aggregate job counts and stack diagnostics. `totalJobCount`, `finishedJobCount`, `stoppedJobCount`, `failedJobCount`, stack type counts, and total stack high-water data are lifetime counters since `init()`. `runningJobCount` and `sleepingJobCount` describe currently active jobs.
`WorkerDiag` reports current state only:

- active, running, sleeping, stopping, and cleanup-queued job counts;
- cleanup-task running state;
- cleanup queue depth and high-water mark.

Completed job records are retained after they reach `Finished`, `Stopped`, or `Failed`. `WorkerJobDiag` reports state, name, stack config, run count, timing, and stack high-water data while a job is active or retained. After `waitFor()` consumes the job following task cleanup, `clearFinished()` reaps it, or Worker ends, `getJobDiagnostics()` returns `JobNotFound`.
Worker does not retain lifetime job counters. `WorkerJobDiag` is available only while a job is active. After automatic cleanup removes the active record, `getJobDiagnostics()` returns `JobNotFound`.

`waitFor()` and `stopAndWait()` return success only after the job has reached a terminal state and its task entry has completed C++ cleanup. Callback captures and task-entry RAII objects have been released before the job is reaped.
A bounded internal completion window allows `waitFor()` to observe fast jobs after their active records have already been removed. It does not retain callbacks, task handles, names, or full diagnostics.

The Worker destructor performs the same cooperative shutdown without a timeout so running tasks cannot continue after Worker internals are destroyed.
The Worker destructor performs cooperative shutdown without a timeout so tasks cannot outlive Worker internals.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 76 additions & 92 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,19 +2,21 @@

Worker is a FreeRTOS task and cooperative job execution library for ESP32.

Worker helps you run one-off and recurring background work in Arduino ESP32 projects with explicit task configuration, cooperative stop/sleep controls, event reporting, and diagnostics. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the app.
Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application.

[![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md)

## Why use Worker?

* **Task-per-job execution** - each `once()` and `every()` job owns a FreeRTOS task.
* **Safe recurring jobs** - `every()` applies the interval delay internally after each callback.
* **ESP32 task control** - configure byte stack size, priority, core affinity, and stack memory preference.
* **Cooperative lifecycle** - jobs can stop or sleep through `WorkerJobContext`.
* **Production-minded** - result-based errors, event callbacks, diagnostics, thread-safe internals, and no intentional exceptions.
- **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task.
- **Automatic cleanup** — callers never need to reap completed jobs.
- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`.
- **Safe recurring jobs** — `every()` applies the interval after each callback.
- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference.
- **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`.
- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history.

## Install

Expand All@@ -27,19 +29,17 @@ board = esp32dev
framework = arduino

lib_deps =
https://github.com/ZekStack/worker.git
https://github.com/ZekStack/worker.git

build_flags =
-std=gnu++20
-std=gnu++20
build_unflags =
-std=gnu++11
-std=gnu++11
```

### Arduino IDE

Worker is not published to Arduino Library Manager yet.

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.
Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory.

```txt
Arduino/libraries/Worker
Expand All@@ -55,60 +55,79 @@ Worker worker;
WorkerJobId recurringJob = 0;

void setup() {
Serial.begin(115200);

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
return;
}

worker.once([](WorkerJobContext &ctx) {
Serial.printf("one-off job id=%u\n", static_cast<unsigned>(ctx.id()));
});

WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {
Serial.printf("run=%u\n", static_cast<unsigned>(ctx.runCount()));
if (ctx.runCount() >= 5) {
ctx.stop();
}
});

if (result) {
recurringJob = result.jobId;
}
Serial.begin(115200);

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
return;
}

worker.once([](WorkerJobContext &ctx) {
Serial.printf("one-off job id=%u\n", static_cast<unsigned>(ctx.id()));
});

WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {
Serial.printf("run=%u\n", static_cast<unsigned>(ctx.runCount()));
if (ctx.runCount() >= 5) {
ctx.stop();
}
});

if (result) {
recurringJob = result.jobId;
}
}

void loop() {
delay(1000);
delay(1000);
}
```

No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically.

## Cleanup model

Worker creates one long-lived internal cleanup task during `init()`.

When a job callback finishes, the job:

1. releases its stored callback;
2. queues its handle and immutable allocation type;
3. suspends itself.

The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns.

This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself.

`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup.

## Important notes

> [!IMPORTANT]
> Worker cancellation is cooperative. `stop()` requests that a job stops and wakes it if it is sleeping, but it does not force-delete a running callback.

* A callback that blocks forever will prevent `stopAndWait()` and timed `end()` calls from completing before their timeout. The destructor waits without a timeout so tasks cannot outlive Worker internals.
* `every(intervalMs, callback)` delays internally after each callback.
* Completed jobs are retained until `waitFor()` can consume them after task cleanup, `clearFinished()` is called, or Worker ends.
* Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
* Custom task names are copied into a fixed internal buffer and may be truncated.
* `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
* `WorkerEvent::type` clearly identifies `Info`, `Warning`, or `Error` events.
* Worker APIs use result objects for normal failures. Catastrophic STL allocation failure while constructing result messages, callbacks, or internal containers is not recoverable by Worker on platforms where the standard library throws or aborts.
> Worker cancellation is cooperative. `stop()` requests that a job stops and wakes it if it is sleeping, but it does not interrupt a running callback.

- A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing.
- The destructor waits without a timeout so tasks cannot outlive Worker internals.
- `every(intervalMs, callback)` delays after each callback.
- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
- `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity.
- `clearFinished()` is retained only as a deprecated compatibility no-op.
- Completion synchronization uses a small bounded token window and never retains callbacks or full completed records.
- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts.

## Examples

| Example | Description |
| --- | --- |
| `Basic` | Minimal init, one-off job, recurring job, wait, and cooperative stop. |
| `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. |
| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. |
| `Events` | Event callback and error event handling. |
| `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. |
| `Diagnostics` | Aggregate diagnostics and active per-job diagnostics. |
| `Diagnostics` | Current job and cleanup-task diagnostics. |
| `BindableCallbacks` | `std::bind` with private class methods. |
| `TaskCleanupSentinel` | Runtime sentinel for verifying task-entry C++ cleanup before `waitFor()` returns. |
| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. |

Start with:

Expand All@@ -118,15 +137,13 @@ examples/Basic

## Documentation

Detailed documentation is available in the `docs/` folder.

| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Step-by-step setup and first job flow. |
| [`docs/configuration.md`](docs/configuration.md) | Config options and stack behavior. |
| [`docs/api.md`](docs/api.md) | Public classes, result types, events, and diagnostics. |
| [`docs/examples.md`](docs/examples.md) | Explanation of all included examples. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common issues and solutions. |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. |
| [`docs/examples.md`](docs/examples.md) | Example descriptions. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. |

## API overview

Expand All@@ -139,62 +156,29 @@ WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {});
WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {});

worker.sleep(loop.jobId, 5000);
worker.stopAndWait(loop.jobId, 2000);

WorkerDiag diag = worker.getDiagnostics();
WorkerJobDiag jobDiag;
worker.getJobDiagnostics(loop.jobId, jobDiag);

worker.stopAndWait(loop.jobId, 2000);
worker.clearFinished();
```

For the full API, see [`docs/api.md`](docs/api.md).

## Compatibility

| Item | Support |
| --- | --- |
| Framework | Arduino ESP32 |
| Platform | `espressif32` |
| Platform | `espressif32` / PIOArduino |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional for task stacks when ESP-IDF support is available |
| PSRAM | Optional task stacks through ESP-IDF capability APIs |
| Dependencies | none |
| Exceptions | Not used |
| Status | Early-stage `0.1.0` |

## Configuration

```cpp
WorkerConfig config;
config.defaultStackSize = 4096;
config.defaultPriority = 1;
config.defaultCoreId = tskNO_AFFINITY;
config.defaultStackType = WorkerStackType::Auto;

WorkerResult result = worker.init(config);
```

For all options, see [`docs/configuration.md`](docs/configuration.md).

## Error handling

Worker reports operation status through `WorkerResult` and `WorkerJobResult`.

```cpp
WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {});

if (!result) {
Serial.println(result.message.c_str());
return;
}
```

For result fields and status codes, see [`docs/api.md`](docs/api.md).

## License

MIT - see [`LICENSE.md`](LICENSE.md).
MIT see [`LICENSE.md`](LICENSE.md).

## ZekStack

Expand Down
60 changes: 39 additions & 21 deletions docs/api.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,36 +6,48 @@ This page summarizes the public API declared in `src/Worker.h`.

Worker does not intentionally throw exceptions. Operations report normal failures through `WorkerResult` or `WorkerJobResult`.

Catastrophic STL allocation failure while constructing result messages, callbacks, or internal containers is not recoverable by Worker on platforms where the standard library throws or aborts.

| Field | Meaning |
| --- | --- |
| `result` | `true` on success, `false` on failure. |
| `status` | Machine-readable `WorkerStatus`. |
| `message` | Human-readable status. |
| `jobId` | Returned by `WorkerJobResult` when a job was created or partially allocated. |
| `jobId` | Returned by `WorkerJobResult` after a job was created. |

`WorkerStatus` values are `Ok`, `NotInitialized`, `AlreadyInitialized`, `InvalidArgument`, `OutOfMemory`, `TaskCreateFailed`, `JobNotFound`, `Busy`, `Timeout`, and `InternalError`.

## Worker

| Method | Purpose |
| --- | --- |
| `init(config)` | Initialize Worker defaults. |
| `init(config)` | Initialize Worker and its cleanup task. |
| `onEvent(callback)` | Register a synchronous event callback. |
| `once(callback)` | Start a one-off task. |
| `once(config, callback)` | Start a configured one-off task. |
| `once(callback)` | Start an automatically cleaned one-off task. |
| `once(config, callback)` | Start a configured automatically cleaned one-off task. |
| `every(intervalMs, callback)` | Start a recurring task with internal delay. |
| `every(intervalMs, config, callback)` | Start a configured recurring task. |
| `stop(jobId)` | Request cooperative stop. |
| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until terminal state and task cleanup. |
| `stop(jobId)` | Request cooperative stop. Cleanup continues automatically. |
| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until the task stack and TCB are released. |
| `sleep(jobId, durationMs)` | Request that a job sleeps. |
| `waitFor(jobId)` | Wait until a registered or retained job reaches terminal state and task cleanup. |
| `waitFor(jobId, timeoutMs)` | Wait with timeout until terminal state and task cleanup. |
| `clearFinished()` | Reap retained terminal job records. |
| `getDiagnostics()` | Return aggregate lifetime diagnostics and current active counts. |
| `getJobDiagnostics(jobId, out)` | Fill per-job diagnostics for an active or retained terminal job. |
| `end(timeoutMs)` | Stop jobs and end Worker, returning `Timeout` if callbacks do not finish in time. |
| `waitFor(jobId)` | Optionally wait until physical task cleanup completes. |
| `waitFor(jobId, timeoutMs)` | Wait with timeout until physical task cleanup completes. |
| `clearFinished()` | Deprecated compatibility no-op. Worker cleans jobs automatically. |
| `getDiagnostics()` | Return current runtime and cleanup-task state. |
| `getJobDiagnostics(jobId, out)` | Fill diagnostics for a currently active job. |
| `end(timeoutMs)` | Stop jobs, drain cleanup, and stop Worker infrastructure. |

`once()` and `every()` are safe for fire-and-forget use. A caller never needs `waitFor()` or `clearFinished()` to release Worker-owned resources.

## Cleanup lifecycle

Worker owns every task it creates. A completed job follows this lifecycle:

1. The callback returns and its stored `std::function` is released.
2. The job queues its task handle to the Worker cleanup task.
3. The job task suspends itself.
4. The cleanup task deletes it externally with `vTaskDelete()` or `vTaskDeleteWithCaps()` as appropriate.
5. Worker records a small bounded completion token and removes the full active job record.

`waitFor()` and `stopAndWait()` succeed only after step 4. They are synchronization APIs, not cleanup APIs.

## Events

Expand All@@ -47,28 +59,34 @@ worker.onEvent([](WorkerEvent event) {
});
```

`WorkerEvent::type` is the source of truth for event severity. Error events use `WorkerEventType::Error` and include a `WorkerStatus`.
Completion events are emitted after physical task deletion completes.

## Job Context
## Job context

Callbacks receive `WorkerJobContext&`.

| Method | Purpose |
| --- | --- |
| `id()` | Return the current job id. |
| `id()` | Return the current job ID. |
| `stop()` | Request that the current job stops. |
| `sleep(durationMs)` | Sleep the current job cooperatively. |
| `shouldStop()` | Check the cooperative stop flag. |
| `runCount()` | Number of callback runs started. |
| `startedAtMs()` | First run time from `millis()`. |
| `lastRunAtMs()` | Most recent run time from `millis()`. |

The context is valid only during callback execution.

## Diagnostics

`WorkerDiag` reports aggregate job counts and stack diagnostics. `totalJobCount`, `finishedJobCount`, `stoppedJobCount`, `failedJobCount`, stack type counts, and total stack high-water data are lifetime counters since `init()`. `runningJobCount` and `sleepingJobCount` describe currently active jobs.
`WorkerDiag` reports current state only:

- active, running, sleeping, stopping, and cleanup-queued job counts;
- cleanup-task running state;
- cleanup queue depth and high-water mark.

Completed job records are retained after they reach `Finished`, `Stopped`, or `Failed`. `WorkerJobDiag` reports state, name, stack config, run count, timing, and stack high-water data while a job is active or retained. After `waitFor()` consumes the job following task cleanup, `clearFinished()` reaps it, or Worker ends, `getJobDiagnostics()` returns `JobNotFound`.
Worker does not retain lifetime job counters. `WorkerJobDiag` is available only while a job is active. After automatic cleanup removes the active record, `getJobDiagnostics()` returns `JobNotFound`.

`waitFor()` and `stopAndWait()` return success only after the job has reached a terminal state and its task entry has completed C++ cleanup. Callback captures and task-entry RAII objects have been released before the job is reaped.
A bounded internal completion window allows `waitFor()` to observe fast jobs after their active records have already been removed. It does not retain callbacks, task handles, names, or full diagnostics.

The Worker destructor performs the same cooperative shutdown without a timeout so running tasks cannot continue after Worker internals are destroyed.
The Worker destructor performs cooperative shutdown without a timeout so tasks cannot outlive Worker internals.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 76 additions & 92 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,19 +2,21 @@

Worker is a FreeRTOS task and cooperative job execution library for ESP32.

Worker helps you run one-off and recurring background work in Arduino ESP32 projects with explicit task configuration, cooperative stop/sleep controls, event reporting, and diagnostics. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the app.
Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application.

[![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md)

## Why use Worker?

* **Task-per-job execution** - each `once()` and `every()` job owns a FreeRTOS task.
* **Safe recurring jobs** - `every()` applies the interval delay internally after each callback.
* **ESP32 task control** - configure byte stack size, priority, core affinity, and stack memory preference.
* **Cooperative lifecycle** - jobs can stop or sleep through `WorkerJobContext`.
* **Production-minded** - result-based errors, event callbacks, diagnostics, thread-safe internals, and no intentional exceptions.
- **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task.
- **Automatic cleanup** — callers never need to reap completed jobs.
- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`.
- **Safe recurring jobs** — `every()` applies the interval after each callback.
- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference.
- **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`.
- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history.

## Install

Expand All@@ -27,19 +29,17 @@ board = esp32dev
framework = arduino

lib_deps =
https://github.com/ZekStack/worker.git
https://github.com/ZekStack/worker.git

build_flags =
-std=gnu++20
-std=gnu++20
build_unflags =
-std=gnu++11
-std=gnu++11
```

### Arduino IDE

Worker is not published to Arduino Library Manager yet.

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.
Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory.

```txt
Arduino/libraries/Worker
Expand All@@ -55,60 +55,79 @@ Worker worker;
WorkerJobId recurringJob = 0;

void setup() {
Serial.begin(115200);

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
return;
}

worker.once([](WorkerJobContext &ctx) {
Serial.printf("one-off job id=%u\n", static_cast<unsigned>(ctx.id()));
});

WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {
Serial.printf("run=%u\n", static_cast<unsigned>(ctx.runCount()));
if (ctx.runCount() >= 5) {
ctx.stop();
}
});

if (result) {
recurringJob = result.jobId;
}
Serial.begin(115200);

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
return;
}

worker.once([](WorkerJobContext &ctx) {
Serial.printf("one-off job id=%u\n", static_cast<unsigned>(ctx.id()));
});

WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {
Serial.printf("run=%u\n", static_cast<unsigned>(ctx.runCount()));
if (ctx.runCount() >= 5) {
ctx.stop();
}
});

if (result) {
recurringJob = result.jobId;
}
}

void loop() {
delay(1000);
delay(1000);
}
```

No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically.

## Cleanup model

Worker creates one long-lived internal cleanup task during `init()`.

When a job callback finishes, the job:

1. releases its stored callback;
2. queues its handle and immutable allocation type;
3. suspends itself.

The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns.

This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself.

`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup.

## Important notes

> [!IMPORTANT]
> Worker cancellation is cooperative. `stop()` requests that a job stops and wakes it if it is sleeping, but it does not force-delete a running callback.

* A callback that blocks forever will prevent `stopAndWait()` and timed `end()` calls from completing before their timeout. The destructor waits without a timeout so tasks cannot outlive Worker internals.
* `every(intervalMs, callback)` delays internally after each callback.
* Completed jobs are retained until `waitFor()` can consume them after task cleanup, `clearFinished()` is called, or Worker ends.
* Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
* Custom task names are copied into a fixed internal buffer and may be truncated.
* `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
* `WorkerEvent::type` clearly identifies `Info`, `Warning`, or `Error` events.
* Worker APIs use result objects for normal failures. Catastrophic STL allocation failure while constructing result messages, callbacks, or internal containers is not recoverable by Worker on platforms where the standard library throws or aborts.
> Worker cancellation is cooperative. `stop()` requests that a job stops and wakes it if it is sleeping, but it does not interrupt a running callback.

- A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing.
- The destructor waits without a timeout so tasks cannot outlive Worker internals.
- `every(intervalMs, callback)` delays after each callback.
- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
- `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity.
- `clearFinished()` is retained only as a deprecated compatibility no-op.
- Completion synchronization uses a small bounded token window and never retains callbacks or full completed records.
- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts.

## Examples

| Example | Description |
| --- | --- |
| `Basic` | Minimal init, one-off job, recurring job, wait, and cooperative stop. |
| `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. |
| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. |
| `Events` | Event callback and error event handling. |
| `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. |
| `Diagnostics` | Aggregate diagnostics and active per-job diagnostics. |
| `Diagnostics` | Current job and cleanup-task diagnostics. |
| `BindableCallbacks` | `std::bind` with private class methods. |
| `TaskCleanupSentinel` | Runtime sentinel for verifying task-entry C++ cleanup before `waitFor()` returns. |
| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. |

Start with:

Expand All@@ -118,15 +137,13 @@ examples/Basic

## Documentation

Detailed documentation is available in the `docs/` folder.

| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Step-by-step setup and first job flow. |
| [`docs/configuration.md`](docs/configuration.md) | Config options and stack behavior. |
| [`docs/api.md`](docs/api.md) | Public classes, result types, events, and diagnostics. |
| [`docs/examples.md`](docs/examples.md) | Explanation of all included examples. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common issues and solutions. |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. |
| [`docs/examples.md`](docs/examples.md) | Example descriptions. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. |

## API overview

Expand All@@ -139,62 +156,29 @@ WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {});
WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {});

worker.sleep(loop.jobId, 5000);
worker.stopAndWait(loop.jobId, 2000);

WorkerDiag diag = worker.getDiagnostics();
WorkerJobDiag jobDiag;
worker.getJobDiagnostics(loop.jobId, jobDiag);

worker.stopAndWait(loop.jobId, 2000);
worker.clearFinished();
```

For the full API, see [`docs/api.md`](docs/api.md).

## Compatibility

| Item | Support |
| --- | --- |
| Framework | Arduino ESP32 |
| Platform | `espressif32` |
| Platform | `espressif32` / PIOArduino |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional for task stacks when ESP-IDF support is available |
| PSRAM | Optional task stacks through ESP-IDF capability APIs |
| Dependencies | none |
| Exceptions | Not used |
| Status | Early-stage `0.1.0` |

## Configuration

```cpp
WorkerConfig config;
config.defaultStackSize = 4096;
config.defaultPriority = 1;
config.defaultCoreId = tskNO_AFFINITY;
config.defaultStackType = WorkerStackType::Auto;

WorkerResult result = worker.init(config);
```

For all options, see [`docs/configuration.md`](docs/configuration.md).

## Error handling

Worker reports operation status through `WorkerResult` and `WorkerJobResult`.

```cpp
WorkerJobResult result = worker.every(1000, [](WorkerJobContext &ctx) {});

if (!result) {
Serial.println(result.message.c_str());
return;
}
```

For result fields and status codes, see [`docs/api.md`](docs/api.md).

## License

MIT - see [`LICENSE.md`](LICENSE.md).
MIT see [`LICENSE.md`](LICENSE.md).

## ZekStack

Expand Down
60 changes: 39 additions & 21 deletions docs/api.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,36 +6,48 @@ This page summarizes the public API declared in `src/Worker.h`.

Worker does not intentionally throw exceptions. Operations report normal failures through `WorkerResult` or `WorkerJobResult`.

Catastrophic STL allocation failure while constructing result messages, callbacks, or internal containers is not recoverable by Worker on platforms where the standard library throws or aborts.

| Field | Meaning |
| --- | --- |
| `result` | `true` on success, `false` on failure. |
| `status` | Machine-readable `WorkerStatus`. |
| `message` | Human-readable status. |
| `jobId` | Returned by `WorkerJobResult` when a job was created or partially allocated. |
| `jobId` | Returned by `WorkerJobResult` after a job was created. |

`WorkerStatus` values are `Ok`, `NotInitialized`, `AlreadyInitialized`, `InvalidArgument`, `OutOfMemory`, `TaskCreateFailed`, `JobNotFound`, `Busy`, `Timeout`, and `InternalError`.

## Worker

| Method | Purpose |
| --- | --- |
| `init(config)` | Initialize Worker defaults. |
| `init(config)` | Initialize Worker and its cleanup task. |
| `onEvent(callback)` | Register a synchronous event callback. |
| `once(callback)` | Start a one-off task. |
| `once(config, callback)` | Start a configured one-off task. |
| `once(callback)` | Start an automatically cleaned one-off task. |
| `once(config, callback)` | Start a configured automatically cleaned one-off task. |
| `every(intervalMs, callback)` | Start a recurring task with internal delay. |
| `every(intervalMs, config, callback)` | Start a configured recurring task. |
| `stop(jobId)` | Request cooperative stop. |
| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until terminal state and task cleanup. |
| `stop(jobId)` | Request cooperative stop. Cleanup continues automatically. |
| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until the task stack and TCB are released. |
| `sleep(jobId, durationMs)` | Request that a job sleeps. |
| `waitFor(jobId)` | Wait until a registered or retained job reaches terminal state and task cleanup. |
| `waitFor(jobId, timeoutMs)` | Wait with timeout until terminal state and task cleanup. |
| `clearFinished()` | Reap retained terminal job records. |
| `getDiagnostics()` | Return aggregate lifetime diagnostics and current active counts. |
| `getJobDiagnostics(jobId, out)` | Fill per-job diagnostics for an active or retained terminal job. |
| `end(timeoutMs)` | Stop jobs and end Worker, returning `Timeout` if callbacks do not finish in time. |
| `waitFor(jobId)` | Optionally wait until physical task cleanup completes. |
| `waitFor(jobId, timeoutMs)` | Wait with timeout until physical task cleanup completes. |
| `clearFinished()` | Deprecated compatibility no-op. Worker cleans jobs automatically. |
| `getDiagnostics()` | Return current runtime and cleanup-task state. |
| `getJobDiagnostics(jobId, out)` | Fill diagnostics for a currently active job. |
| `end(timeoutMs)` | Stop jobs, drain cleanup, and stop Worker infrastructure. |

`once()` and `every()` are safe for fire-and-forget use. A caller never needs `waitFor()` or `clearFinished()` to release Worker-owned resources.

## Cleanup lifecycle

Worker owns every task it creates. A completed job follows this lifecycle:

1. The callback returns and its stored `std::function` is released.
2. The job queues its task handle to the Worker cleanup task.
3. The job task suspends itself.
4. The cleanup task deletes it externally with `vTaskDelete()` or `vTaskDeleteWithCaps()` as appropriate.
5. Worker records a small bounded completion token and removes the full active job record.

`waitFor()` and `stopAndWait()` succeed only after step 4. They are synchronization APIs, not cleanup APIs.

## Events

Expand All@@ -47,28 +59,34 @@ worker.onEvent([](WorkerEvent event) {
});
```

`WorkerEvent::type` is the source of truth for event severity. Error events use `WorkerEventType::Error` and include a `WorkerStatus`.
Completion events are emitted after physical task deletion completes.

## Job Context
## Job context

Callbacks receive `WorkerJobContext&`.

| Method | Purpose |
| --- | --- |
| `id()` | Return the current job id. |
| `id()` | Return the current job ID. |
| `stop()` | Request that the current job stops. |
| `sleep(durationMs)` | Sleep the current job cooperatively. |
| `shouldStop()` | Check the cooperative stop flag. |
| `runCount()` | Number of callback runs started. |
| `startedAtMs()` | First run time from `millis()`. |
| `lastRunAtMs()` | Most recent run time from `millis()`. |

The context is valid only during callback execution.

## Diagnostics

`WorkerDiag` reports aggregate job counts and stack diagnostics. `totalJobCount`, `finishedJobCount`, `stoppedJobCount`, `failedJobCount`, stack type counts, and total stack high-water data are lifetime counters since `init()`. `runningJobCount` and `sleepingJobCount` describe currently active jobs.
`WorkerDiag` reports current state only:

- active, running, sleeping, stopping, and cleanup-queued job counts;
- cleanup-task running state;
- cleanup queue depth and high-water mark.

Completed job records are retained after they reach `Finished`, `Stopped`, or `Failed`. `WorkerJobDiag` reports state, name, stack config, run count, timing, and stack high-water data while a job is active or retained. After `waitFor()` consumes the job following task cleanup, `clearFinished()` reaps it, or Worker ends, `getJobDiagnostics()` returns `JobNotFound`.
Worker does not retain lifetime job counters. `WorkerJobDiag` is available only while a job is active. After automatic cleanup removes the active record, `getJobDiagnostics()` returns `JobNotFound`.

`waitFor()` and `stopAndWait()` return success only after the job has reached a terminal state and its task entry has completed C++ cleanup. Callback captures and task-entry RAII objects have been released before the job is reaped.
A bounded internal completion window allows `waitFor()` to observe fast jobs after their active records have already been removed. It does not retain callbacks, task handles, names, or full diagnostics.

The Worker destructor performs the same cooperative shutdown without a timeout so running tasks cannot continue after Worker internals are destroyed.
The Worker destructor performs cooperative shutdown without a timeout so tasks cannot outlive Worker internals.
Loading
Loading