ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
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
16#if defined(ARDUINO)
17#include "freertos/FreeRTOS.h"
18#include "freertos/queue.h"
19#include "freertos/task.h"
20#endif
21namespace
22{
23// Per-task worker id, for a genuine multi-worker build. Default 0: the user loop(), the lwIP
24// thread, and unit tests all read worker 0. With PC_WORKER_COUNT == 1 pc_worker_self() answers 0
25// inline (see worker.h) and this TLS slot is never read on the hot path.
26thread_local int t_worker_id = 0;
27} // namespace
28
30{
31 return PC_WORKER_COUNT;
32}
33
34#if PC_WORKER_COUNT != 1
36{
37 return t_worker_id;
38}
39#endif
40
42{
43 t_worker_id = id;
44}
45
46// ---------------------------------------------------------------------------
47// Worker tasks
48// ---------------------------------------------------------------------------
49
50#ifdef ARDUINO
51
52namespace
53{
54// All worker-task state, owned by one instance (internal linkage): the pump callback, the task
55// handles, and the run flag. One named owner, unreachable from any other translation unit.
56struct WorkerCtx
57{
58 pc_worker_pump_fn pump = nullptr;
59 TaskHandle_t tasks[PC_WORKER_COUNT] = {nullptr};
60 std::atomic<bool> run{false}; // release on start publishes pump; acquire in the task
61};
62WorkerCtx s_worker;
63
64// Each worker binds its id, then pumps until asked to stop. Between iterations it
65// blocks on its task notification instead of free-running the poll: a producer
66// (listener_enqueue, pc_defer) nudges it the moment work arrives, so events are
67// serviced immediately rather than on the next tick. The block still times out
68// after PC_WORKER_POLL_TICKS so the idle timeout sweep (check_timeouts) keeps
69// reaping stale connections with no events in flight; raising that knob now lowers
70// idle wakeups without costing event latency. A nudge that races the pump is
71// latched in the notify count, so ulTaskNotifyTake returns at once - no lost wake.
72void worker_task(void *arg)
73{
74 int id = (int)(intptr_t)arg;
76 while (s_worker.run.load(std::memory_order_acquire))
77 {
78 if (s_worker.pump)
79 {
80 s_worker.pump(id);
81 }
82 ulTaskNotifyTake(pdTRUE, PC_WORKER_POLL_TICKS); // wake on event, else idle-sweep timeout
83 }
84 s_worker.tasks[id] = nullptr;
85 vTaskDelete(nullptr);
86}
87} // namespace
88
89// Per-worker deferred-callback queues: app code on any task hands a {fn, arg} to
90// the owning worker, which runs it in its own context (race-free push path).
91namespace
92{
93struct DeferCmd
94{
96 void *arg;
97};
98// The per-worker deferred-callback queue HANDLES, owned by one instance. The hot path
99// (pc_defer / run_deferred / wake) touches only these, so this stays small and live.
100struct DeferCtx
101{
102 QueueHandle_t dq[PC_WORKER_COUNT] = {nullptr};
103};
104DeferCtx s_defer;
105
106// The FreeRTOS static-queue backing store (control blocks + byte storage), in its OWN
107// owned instance. Only pc_workers_start() references it (to create the queues), so a
108// firmware that never starts workers (e.g. a pure client sketch) garbage-collects this
109// multi-hundred-byte store instead of anchoring it through the always-live handle path.
110struct DeferStorageCtx
111{
112 StaticQueue_t dq_struct[PC_WORKER_COUNT];
113 uint8_t dq_storage[PC_WORKER_COUNT][PC_DEFER_QUEUE_DEPTH * sizeof(DeferCmd)];
114};
115DeferStorageCtx s_defer_store;
116} // namespace
117
119{
120 if (s_worker.run.load(std::memory_order_acquire))
121 {
122 return; // already running
123 }
124 s_worker.pump = pump;
125 for (int i = 0; i < PC_WORKER_COUNT; i++)
126 {
127 if (!s_defer.dq[i])
128 {
129 s_defer.dq[i] = xQueueCreateStatic(PC_DEFER_QUEUE_DEPTH, sizeof(DeferCmd), s_defer_store.dq_storage[i],
130 &s_defer_store.dq_struct[i]);
131 }
132 }
133 s_worker.run.store(true, std::memory_order_release);
134 for (int i = 0; i < PC_WORKER_COUNT; i++)
135 {
136 int core = (PC_WORKER_CORE + i) % portNUM_PROCESSORS;
137 xTaskCreatePinnedToCore(worker_task, "pc_worker", PC_WORKER_TASK_STACK, (void *)(intptr_t)i,
138 PC_WORKER_TASK_PRIORITY, &s_worker.tasks[i], core);
139 }
140}
141
142bool pc_defer(int worker_id, pc_deferred_fn fn, void *arg)
143{
144 if (!fn)
145 {
146 return false;
147 }
148 if (worker_id < 0 || worker_id >= PC_WORKER_COUNT || !s_defer.dq[worker_id])
149 {
150 return false;
151 }
152 DeferCmd cmd = {fn, arg};
153 if (xQueueSend(s_defer.dq[worker_id], &cmd, 0) != pdTRUE)
154 {
155 return false;
156 }
157 pc_worker_wake(worker_id); // run the callback now, not on the next idle sweep
158 return true;
159}
160
161void pc_worker_wake(int worker_id)
162{
163 if (worker_id < 0 || worker_id >= PC_WORKER_COUNT)
164 {
165 return;
166 }
167 TaskHandle_t t = s_worker.tasks[worker_id];
168 if (t)
169 {
170 xTaskNotifyGive(t);
171 }
172}
173
174void pc_worker_run_deferred(int worker_id)
175{
176 if (worker_id < 0 || worker_id >= PC_WORKER_COUNT || !s_defer.dq[worker_id])
177 {
178 return;
179 }
180 DeferCmd cmd;
181 while (xQueueReceive(s_defer.dq[worker_id], &cmd, 0) == pdTRUE)
182 {
183 if (cmd.fn)
184 {
185 cmd.fn(cmd.arg);
186 }
187 }
188}
189
191{
192 if (!s_worker.run.load(std::memory_order_acquire))
193 {
194 return;
195 }
196 s_worker.run.store(false, std::memory_order_release);
197 // Tasks self-delete on their next iteration; give them a few ticks to exit
198 // before the caller tears down the slots they were servicing.
199 vTaskDelay(3);
200}
201
203{
204 return s_worker.run.load(std::memory_order_acquire);
205}
206
207#else // host build - no tasks; handle()/tests drive the pipeline inline
208
210{
211}
212void pc_workers_stop(void)
213{
214}
215bool pc_workers_running(void)
216{
217 return false;
218}
219void pc_worker_wake(int)
220{
221} // no worker task on host - nothing to wake
222
223// No worker task on host: the caller and the pipeline are the same thread, so a
224// deferred callback can run inline immediately (same observable effect, race-free).
225bool pc_defer(int, pc_deferred_fn fn, void *arg)
226{
227 if (!fn)
228 {
229 return false;
230 }
231 fn(arg);
232 return true;
233}
235{
236}
237
238#endif // ARDUINO
#define PC_DEFER_QUEUE_DEPTH
Depth of each worker's deferred-callback queue.
#define PC_WORKER_TASK_PRIORITY
FreeRTOS priority for each server worker task (ESP32).
#define PC_WORKER_COUNT
Number of server worker tasks (slots partitioned i % N). Default 1.
#define PC_WORKER_TASK_STACK
#define PC_WORKER_CORE
Core that worker 0 pins to (ESP32). Worker k pins to (PC_WORKER_CORE.
#define PC_WORKER_POLL_TICKS
Idle-sweep timeout, in FreeRTOS ticks, that a worker blocks between service iterations when no events...
void pc_worker_run_deferred(int worker_id)
Drain and run worker worker_id's deferred callbacks (called by the worker).
Definition worker.cpp:174
void pc_worker_wake(int worker_id)
Wake worker worker_id so it services a freshly-queued event now.
Definition worker.cpp:161
void pc_workers_start(pc_worker_pump_fn pump)
Spawn the worker task(s) and start them running pump. No-op on host.
Definition worker.cpp:118
bool pc_workers_running(void)
True while worker task(s) are running (always false on host).
Definition worker.cpp:202
void pc_worker_set_self(int id)
Bind the calling task/thread to worker id id (worker entry / tests).
Definition worker.cpp:41
int pc_worker_count(void)
Number of server worker tasks (PC_WORKER_COUNT).
Definition worker.cpp:29
bool pc_defer(int worker_id, pc_deferred_fn fn, void *arg)
Run fn(arg) on worker worker_id. Returns false if the queue is full.
Definition worker.cpp:142
void pc_workers_stop(void)
Signal the worker task(s) to exit and wait briefly for them. No-op on host.
Definition worker.cpp:190
int pc_worker_self(void)
Worker id [0, count) of the calling task; 0 by default / single-worker.
Definition worker.cpp:35
Layer 5 (Session) - server worker identity.
void(* pc_worker_pump_fn)(int worker_id)
Pump callback run by each worker task with its worker id.
Definition worker.h:65
void(* pc_deferred_fn)(void *arg)
Deferred callback signature.
Definition worker.h:107