FreeRTOS Queue Sets: Keep Selection and Consumption in One Ownership Contract
A queue set works reliably when selecting a member and consuming one item from that member are treated as a single application-level obligation. Do not let a diagnostic task, a secondary worker or a recovery shortcut read a member independently. Otherwise, the set can retain a readiness event after the corresponding payload has disappeared.
This guide is scoped to FreeRTOS Kernel V11.1.0, ordinary queues and binary or counting semaphores, with queue sets enabled. The API contract comes from the versioned queue header; the state trace below is an engineering explanation of the matching implementation. Vendor forks, wrappers and different kernel revisions need their own inspection. The proposed tests have not been executed on target hardware.
Separate the payload from the readiness event
Think of a member queue as holding application data and the set as holding member handles. In V11.1.0, selecting from the set receives a handle from the set’s own queue. It does not receive the member’s payload. The notification path places the member handle into that set queue. These are two distinct operations, as shown in the versioned implementation linked above.
A useful application invariant follows: each successful selection should be followed by one corresponding receive or semaphore take before that selection is considered handled. This is a recommended ownership discipline, rather than a new kernel API. Keep the pair in the same dispatcher wherever possible. If work is forwarded to another task, forward the already-received application item; avoid delegating a bare member handle whose consumption can race with other paths.

Make the mismatch visible with a controlled trace
Use a single member queue Q and set S in an isolated test build. Add Q while it is empty and check the return value. Prevent unrelated producers and consumers from running during each step. The following trace deliberately violates the documented contract to expose the failure mechanism; it is not a recovery recipe.
- Begin with Q empty and no pending selections in S. Send item A to Q and confirm that the send succeeds.
- Before the intended dispatcher selects anything, have a test-only path receive A directly from Q. Record the successful receive.
- Let the dispatcher select from S. Record whether Q is returned.
- Immediately attempt a zero-timeout receive from Q. A returned Q followed by an empty receive is the evidence of a stale readiness event.
- Recreate the isolated objects for the control run. Send A, select Q through S, then receive A. The selection and payload should now agree.
The expected negative-test behavior is an inference from the two-queue implementation, not a measured result presented here. A different outcome should trigger inspection of the actual build, wrapper behavior and intervening task execution. Do not explain it away by changing timeout values.
A long blocking receive in step four can hide the defect: a later producer may deliver item B, allowing the dispatcher to continue with the wrong temporal relationship. That is why the diagnostic receive should use zero wait and record an explicit mismatch. In normal code, a failed immediate receive after a valid selection deserves an error path and a trace record, not an unconditional assertion that silently disappears from release builds.
Audit every route that can consume a member
Search by handle ownership, not only by API name. A wrapper around a receive can obscure the second consumer. Include startup routines, service modes, test hooks, timeout cleanup and shutdown logic. For each route, answer four questions: who selects, who consumes, how many items are consumed, and what prevents another route from consuming between those operations?
- A dispatcher that selects once and drains every item from that member breaks the proposed one-selection/one-consumption discipline. Return to set selection between items.
- A debugger-facing task should inspect copied application state rather than steal live queue items.
- A timeout path must not interpret an old remembered handle as a fresh selection.
- A reset or teardown path needs quiesced producers and a deliberate lifecycle protocol. Clearing payloads alone is not proof that queued readiness records have disappeared.
Keep any proposed multi-consumer design out of the initial qualification. First prove the single-owner contract. If the product genuinely needs separate consumers, document and test the transfer of each selection obligation explicitly; task priority alone is not an ownership mechanism.
Check membership and event capacity before blaming scheduling
The V11.1.0 API specifies event capacity based on the sum of member capacities, counting a binary semaphore as one and a counting semaphore by its maximum count. It also warns that a task blocked on a set containing a mutex does not confer priority inheritance on the mutex holder. These details belong in the design review, even when the observed symptom looks like a scheduling problem. See the queue-set notes in the V11.1.0 header.
For this ownership-focused investigation, use enough set capacity to remove overflow as a competing explanation. Check every creation and membership result. In the implementation, adding a nonempty member is rejected; a preloaded queue therefore needs explicit attention during initialization. Confirm that producers cannot start early and make membership timing nondeterministic.
Capacity is necessary but does not repair an invalid consumer. Increasing the set length may merely postpone the visible symptom while stale events continue accumulating. Keep separate counters for successful sends, selections, successful consumes and empty-after-selection observations. Compare them only at a defined quiescent checkpoint; live snapshots taken at different times can disagree legitimately.
Hypothetical example: a service console steals a completion
Consider a hypothetical controller whose dispatcher waits on a sensor-result queue and a binary service semaphore. A maintenance console recently gained a “show latest result” command. Its implementation reads the sensor queue directly, because that was simpler than introducing a snapshot structure.
The console displays a valid result. Later, the dispatcher receives that queue’s handle from the set but finds no payload. A developer adds a longer receive timeout, and the next sensor result makes the symptom seem resolved. The missing evidence is the original result’s ownership: the console consumed it before the dispatcher selected it.
A targeted fix would give the dispatcher sole consumption ownership and publish a separate latest-result snapshot after receipt. Qualification would exercise console reads before selection, during dispatch and after dispatch. The desired evidence is no unauthorized queue consumption, no empty-after-selection events and correct result identity at the application boundary. This is a proposed design exercise, not a report of an Obeita deployment.
Release gate: prove the pairing, then stress it
Keep the negative test as a regression test for the instrumentation itself: it should detect the intentionally broken ownership path. Then remove that path and run bursts from every legitimate producer, semaphore events, dispatcher preemption and controlled shutdown. Log item sequence numbers where the application format permits them, without assuming that a sequence gap alone proves a queue-set defect.
Accept the implementation only when the ownership audit and the observed trace agree. If failures remain, preserve the first mismatch with the selected handle, consuming task, operation result and build identity. That compact record is more useful than a large log that merely says the dispatcher woke up.