DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
worker.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file worker.cpp
6 * @brief Server worker identity - implementation.
7 *
8 * The id lives in thread-local storage so each worker task resolves its own
9 * per-worker state with no lock and no shared lookup. The block is part of task
10 * creation (no heap after begin()); an unbound context reads the zero default.
11 */
12
14#include <atomic>
15
16namespace
17{
18// Per-task worker id. Default 0: the user loop(), the lwIP thread, and unit tests
19// all read worker 0, which is the sole worker in the default build.
20thread_local int t_worker_id = 0;
21} // namespace
22
24{
25 return DETWS_WORKER_COUNT;
26}
27
29{
30 return t_worker_id;
31}
32
34{
35 t_worker_id = id;
36}
37
38// ---------------------------------------------------------------------------
39// Worker tasks
40// ---------------------------------------------------------------------------
41
42#ifdef ARDUINO
43
44#include "freertos/FreeRTOS.h"
45#include "freertos/queue.h"
46#include "freertos/task.h"
47
48namespace
49{
50// All worker-task state, owned by one instance (internal linkage): the pump callback, the task
51// handles, and the run flag. One named owner, unreachable from any other translation unit.
52struct WorkerCtx
53{
54 detws_worker_pump_fn pump = nullptr;
55 TaskHandle_t tasks[DETWS_WORKER_COUNT] = {nullptr};
56 std::atomic<bool> run{false}; // release on start publishes pump; acquire in the task
57};
58WorkerCtx s_worker;
59
60// Each worker binds its id, then pumps until asked to stop. Between iterations it
61// blocks on its task notification instead of free-running the poll: a producer
62// (listener_enqueue, detws_defer) nudges it the moment work arrives, so events are
63// serviced immediately rather than on the next tick. The block still times out
64// after DETWS_WORKER_POLL_TICKS so the idle timeout sweep (check_timeouts) keeps
65// reaping stale connections with no events in flight; raising that knob now lowers
66// idle wakeups without costing event latency. A nudge that races the pump is
67// latched in the notify count, so ulTaskNotifyTake returns at once - no lost wake.
68void worker_task(void *arg)
69{
70 int id = (int)(intptr_t)arg;
72 while (s_worker.run.load(std::memory_order_acquire))
73 {
74 if (s_worker.pump)
75 s_worker.pump(id);
76 ulTaskNotifyTake(pdTRUE, DETWS_WORKER_POLL_TICKS); // wake on event, else idle-sweep timeout
77 }
78 s_worker.tasks[id] = nullptr;
79 vTaskDelete(nullptr);
80}
81} // namespace
82
83// Per-worker deferred-callback queues: app code on any task hands a {fn, arg} to
84// the owning worker, which runs it in its own context (race-free push path).
85namespace
86{
87struct DeferCmd
88{
90 void *arg;
91};
92// The per-worker deferred-callback queue HANDLES, owned by one instance. The hot path
93// (detws_defer / run_deferred / wake) touches only these, so this stays small and live.
94struct DeferCtx
95{
96 QueueHandle_t dq[DETWS_WORKER_COUNT] = {nullptr};
97};
98DeferCtx s_defer;
99
100// The FreeRTOS static-queue backing store (control blocks + byte storage), in its OWN
101// owned instance. Only detws_workers_start() references it (to create the queues), so a
102// firmware that never starts workers (e.g. a pure client sketch) garbage-collects this
103// multi-hundred-byte store instead of anchoring it through the always-live handle path.
104struct DeferStorageCtx
105{
106 StaticQueue_t dq_struct[DETWS_WORKER_COUNT];
107 uint8_t dq_storage[DETWS_WORKER_COUNT][DETWS_DEFER_QUEUE_DEPTH * sizeof(DeferCmd)];
108};
109DeferStorageCtx s_defer_store;
110} // namespace
111
113{
114 if (s_worker.run.load(std::memory_order_acquire))
115 return; // already running
116 s_worker.pump = pump;
117 for (int i = 0; i < DETWS_WORKER_COUNT; i++)
118 if (!s_defer.dq[i])
119 s_defer.dq[i] = xQueueCreateStatic(DETWS_DEFER_QUEUE_DEPTH, sizeof(DeferCmd), s_defer_store.dq_storage[i],
120 &s_defer_store.dq_struct[i]);
121 s_worker.run.store(true, std::memory_order_release);
122 for (int i = 0; i < DETWS_WORKER_COUNT; i++)
123 {
124 int core = (DETWS_WORKER_CORE + i) % portNUM_PROCESSORS;
125 xTaskCreatePinnedToCore(worker_task, "detws_worker", DETWS_WORKER_TASK_STACK, (void *)(intptr_t)i,
126 DETWS_WORKER_TASK_PRIORITY, &s_worker.tasks[i], core);
127 }
128}
129
130bool detws_defer(int worker_id, detws_deferred_fn fn, void *arg)
131{
132 if (!fn)
133 return false;
134 if (worker_id < 0 || worker_id >= DETWS_WORKER_COUNT || !s_defer.dq[worker_id])
135 return false;
136 DeferCmd cmd = {fn, arg};
137 if (xQueueSend(s_defer.dq[worker_id], &cmd, 0) != pdTRUE)
138 return false;
139 detws_worker_wake(worker_id); // run the callback now, not on the next idle sweep
140 return true;
141}
142
143void detws_worker_wake(int worker_id)
144{
145 if (worker_id < 0 || worker_id >= DETWS_WORKER_COUNT)
146 return;
147 TaskHandle_t t = s_worker.tasks[worker_id];
148 if (t)
149 xTaskNotifyGive(t);
150}
151
152void detws_worker_run_deferred(int worker_id)
153{
154 if (worker_id < 0 || worker_id >= DETWS_WORKER_COUNT || !s_defer.dq[worker_id])
155 return;
156 DeferCmd cmd;
157 while (xQueueReceive(s_defer.dq[worker_id], &cmd, 0) == pdTRUE)
158 if (cmd.fn)
159 cmd.fn(cmd.arg);
160}
161
163{
164 if (!s_worker.run.load(std::memory_order_acquire))
165 return;
166 s_worker.run.store(false, std::memory_order_release);
167 // Tasks self-delete on their next iteration; give them a few ticks to exit
168 // before the caller tears down the slots they were servicing.
169 vTaskDelay(3);
170}
171
173{
174 return s_worker.run.load(std::memory_order_acquire);
175}
176
177#else // host build - no tasks; handle()/tests drive the pipeline inline
178
180{
181}
182void detws_workers_stop(void)
183{
184}
185bool detws_workers_running(void)
186{
187 return false;
188}
189void detws_worker_wake(int)
190{
191} // no worker task on host - nothing to wake
192
193// No worker task on host: the caller and the pipeline are the same thread, so a
194// deferred callback can run inline immediately (same observable effect, race-free).
195bool detws_defer(int, detws_deferred_fn fn, void *arg)
196{
197 if (!fn)
198 return false;
199 fn(arg);
200 return true;
201}
203{
204}
205
206#endif // ARDUINO
#define DETWS_DEFER_QUEUE_DEPTH
Depth of each worker's deferred-callback queue.
#define DETWS_WORKER_CORE
Core that worker 0 pins to (ESP32). Worker k pins to (DETWS_WORKER_CORE.
#define DETWS_WORKER_TASK_PRIORITY
FreeRTOS priority for each server worker task (ESP32).
#define DETWS_WORKER_COUNT
Number of server worker tasks (slots partitioned i % N). Default 1.
#define DETWS_WORKER_POLL_TICKS
Idle-sweep timeout, in FreeRTOS ticks, that a worker blocks between service iterations when no events...
#define DETWS_WORKER_TASK_STACK
Stack (bytes) for each server worker task (ESP32).
void detws_worker_set_self(int id)
Bind the calling task/thread to worker id id (worker entry / tests).
Definition worker.cpp:33
int detws_worker_count(void)
Number of server worker tasks (DETWS_WORKER_COUNT).
Definition worker.cpp:23
int detws_worker_self(void)
Worker id [0, count) of the calling task; 0 by default / single-worker.
Definition worker.cpp:28
void detws_workers_stop(void)
Signal the worker task(s) to exit and wait briefly for them. No-op on host.
Definition worker.cpp:162
bool detws_workers_running(void)
True while worker task(s) are running (always false on host).
Definition worker.cpp:172
void detws_workers_start(detws_worker_pump_fn pump)
Spawn the worker task(s) and start them running pump. No-op on host.
Definition worker.cpp:112
bool detws_defer(int worker_id, detws_deferred_fn fn, void *arg)
Run fn(arg) on worker worker_id. Returns false if the queue is full.
Definition worker.cpp:130
void detws_worker_wake(int worker_id)
Wake worker worker_id so it services a freshly-queued event now.
Definition worker.cpp:143
void detws_worker_run_deferred(int worker_id)
Drain and run worker worker_id's deferred callbacks (called by the worker).
Definition worker.cpp:152
Layer 5 (Session) - server worker identity.
void(* detws_worker_pump_fn)(int worker_id)
Pump callback run by each worker task with its worker id.
Definition worker.h:50
void(* detws_deferred_fn)(void *arg)
Deferred callback signature.
Definition worker.h:92