FreeRTOS xQueuePeek: Avoid Treating Observation as Ownership

A successful xQueuePeek() copies the current head item without removing it. It does not reserve that item for the caller. If work must be processed exactly once by one consumer, perform the successful receive that claims the item before taking an irreversible action.

This article targets FreeRTOS Kernel V11.1.0 ordinary queues. It examines peek/receive ordering, including replacement and pointer lifetime, rather than queue-set readiness or queue capacity. Vendor wrappers may add their own locks or ownership rules; inspect those rules separately. The selected local tests used the official POSIX port, not an MCU.

Write down what the peek authorizes

The versioned queue API distinguishes a copied peek from a receive that removes an item. The matching implementation saves and restores the queue read position when peeking. Its protection covers that kernel operation, not an arbitrary sequence of application calls afterward.

For every peek call, classify its use. A diagnostic observation of a by-value item may need only a clearly labeled snapshot. Routing, billing, actuator commands or buffer reclamation require a stronger authority: who has claimed the work, and what prevents a second caller from acting on the same item?

Make that distinction visible in the interface. A function returning “observed request” should not return the same application type as “owned request” unless the type carries an explicit claim state. This is an application design recommendation, not a property supplied by the queue handle.

Interleaving in which consumer one peeks A, consumer two receives A, producer sends B, and consumer one then receives B; the peeked identity is not a reservation.
Each queue call can be valid while the combined application assumption is false. The item received later may differ from the item observed earlier.

Reproduce the smallest identity mismatch

Create an isolated queue and use distinct by-value identifiers. Keep the test deterministic: a controller executes the steps or gates two consumers at the relevant boundaries. Do not begin with a random stress test whose ordering cannot be reconstructed.

  1. Publish A and let consumer one peek it. Record the peek result and A’s identifier.
  2. Before consumer one’s receive, let consumer two receive A successfully.
  3. Publish B, then allow consumer one to receive. Require its successful result to be B, not the previously peeked A.
  4. Repeat without publishing B. Consumer one’s immediate receive should report empty, demonstrating that a successful peek is not a guarantee of a future receive.
  5. For a valid single-slot overwrite integration, replace A with B between peek and receive. Again, require the receive result to determine the claimed item.

The first three steps were executed as a scripted sequence against V11.1.0 with the POSIX port and GCC 14.2.0. The assertions passed: peek left occupancy unchanged, the intervening receive removed A, and the later receive returned B. The two-consumer gated version and the additional cases remain proposed target tests. This is an API ordering result, not evidence about how often a production race occurs.

Separate duplicate processing from wrong-item removal

Two defects can arise from the same mistaken reservation assumption. First, both consumers may peek A and perform its side effect before either receives it. The later queue operations cannot undo duplicate processing. Second, consumer one may process its copied A, then receive B while believing it is merely removing the completed A. B has now disappeared without being processed under its own identity.

Checking that the received identifier matches the peeked identifier can detect the second defect after the fact, but it does not make an earlier side effect safe. Logging “unexpected request” is useful evidence, not an ownership repair. Put the claim boundary before the action whose duplication or loss matters.

Do not attempt to push an unexpected received item back blindly. Other items or producers may have changed the queue, and reinsertion changes ordering and admission behavior. A recovery policy needs the application’s actual semantics; a generic requeue loop can compound the failure.

Choose one of three ownership designs

Need Design to qualify Boundary
Distribute independent jobs Receive first; process only that returned item Successful receive is the application claim
Inspect before choosing a worker One dispatcher receives and routes owned work Workers do not independently consume the source queue
Observe status without consuming jobs Publish a separate read-only snapshot Snapshot freshness is separate from work ownership

An application lock can serialize a larger peek-and-claim protocol only if every conflicting path obeys it, including any producer operation that can replace the head. That is more demanding than putting a mutex around one caller. Never hold such a lock across a blocking wait when the task that must satisfy the wait needs the same lock.

If priority determines which task normally runs first, retain explicit synchronization anyway. A debugger pause, preemption, changed priority or additional consumer can expose an ordering that the happy-path schedule concealed. On SMP, simultaneous execution adds another reason to avoid relying on apparent task ordering.

Audit pointer items separately

When the queue item is a pointer, peeking copies that pointer value; it does not copy or retain the pointed-to allocation. Another consumer can receive and release the allocation after the peek. Dereferencing the saved pointer may then be invalid even though the peek itself succeeded.

A reference-counted design needs a safe way to acquire the reference before reclamation becomes possible. Incrementing a count through an already stale pointer is not that mechanism. For status observation, consider copying the required fields under the actual owner’s synchronization instead. Keep the queue’s copy semantics separate from object lifetime.

Hypothetical example and acceptance evidence

Imagine a hypothetical gateway with two upload workers. A helper peeks a job to inspect its destination, opens a connection, then receives the job. While that connection opens, the other worker removes the original job. The first helper now receives a different destination and sends it through a connection chosen for the old one.

A receive-first design gives the helper a specific job before connection selection. Alternatively, one dispatcher receives and routes jobs to destination-specific workers. Qualification should record peeked identity, received identity, acting worker and the moment ownership transfers, using synthetic identifiers rather than private payloads.

Accept the design when each side effect belongs to an owned item, every successful receive has one disposition, and observational callers cannot steal or duplicate work. Include queue-empty returns, replacement, pointer release and delayed workers in the regression matrix. The local scripted check proves the mismatch is possible under valid API calls; hardware scheduling and the complete product ownership protocol still need their own tests.

Similar Posts