# Priority Queue

## Summary

`<span class="editor-theme-code">bucketed_pqueue</span>`<span style="white-space: pre-wrap;"> is a simple, efficient strict-priority queue for FreeRTOS systems where:</span>

- items fall into a small fixed set of priority levels
- producers may be tasks or ISRs
- a single consumer drains work in priority order

Its design is intentionally lightweight:

- one FreeRTOS queue per priority
- one bitmap to track which priorities are active
- optional task notification for efficient wakeup

<p class="callout success">Used correctly, it is a clean fit for embedded event dispatch and deferred work handling.</p>

<p class="callout warning">Used incorrectly, mainly with multiple consumers or inconsistent bucket item types, it becomes a fine little trap with excellent timing and poor manners.</p>

## Purpose

<p class="callout info"><span style="white-space: pre-wrap;">The </span>`<span class="editor-theme-code">bucketed_pqueue</span>`<span style="white-space: pre-wrap;"> module implements a </span>**strict-priority queue**<span style="white-space: pre-wrap;"> on top of standard FreeRTOS queues.</span></p>

<span style="white-space: pre-wrap;">Instead of storing all items in one queue, it uses </span>**multiple FIFO queues**<span style="white-space: pre-wrap;">, called </span>**buckets**, where each bucket represents one priority level.

- **Bucket 0**<span style="white-space: pre-wrap;"> = lowest priority</span>
- **Bucket `<strong class="editor-theme-bold editor-theme-code">num_buckets - 1</strong>`**<span style="white-space: pre-wrap;"> = highest priority</span>

<span style="white-space: pre-wrap;">When consuming items, the module always returns an item from the </span>**highest non-empty priority bucket**.

<span style="white-space: pre-wrap;">Within the same priority bucket, ordering remains </span>**FIFO**, because each bucket is an ordinary FreeRTOS queue.

This gives the system:

- **strict priority across buckets**
- **FIFO ordering within each bucket**
- <span style="white-space: pre-wrap;">support for </span>**task producers**
- <span style="white-space: pre-wrap;">support for </span>**ISR producers**
- a lightweight way to wake a consumer task when new work arrives

## When to use this module

Use this module when:

- <span style="white-space: pre-wrap;">work items have a </span>**small fixed number of priority levels**
- **higher-priority items must always be processed first**
- you want to keep FreeRTOS queue semantics
- <span style="white-space: pre-wrap;">producers may run in both </span>**task**<span style="white-space: pre-wrap;"> and </span>**interrupt**<span style="white-space: pre-wrap;"> context</span>
- <span style="white-space: pre-wrap;">only </span>**one consumer**<span style="white-space: pre-wrap;"> is responsible for draining the queue</span>

Typical use cases:

- event dispatching where alarms/errors must preempt normal work
- deferred interrupt processing with different urgency levels
- message handling where command classes have discrete priority bands

## <span style="white-space: pre-wrap;">When to </span>**not** use this module

<p class="callout warning"><span style="white-space: pre-wrap;">This module is </span>**not**<span style="white-space: pre-wrap;"> a general-purpose concurrent priority queue.</span></p>

<span style="white-space: pre-wrap;">Do </span>**not**<span style="white-space: pre-wrap;"> use it if:</span>

- <span style="white-space: pre-wrap;">you need </span>**multiple consumer tasks**<span style="white-space: pre-wrap;"> calling </span>`<span class="editor-theme-code">pop()</span>`<span style="white-space: pre-wrap;"> or </span>`<span class="editor-theme-code">peek()</span>`<span style="white-space: pre-wrap;"> concurrently</span>
- <span style="white-space: pre-wrap;">you need </span>**arbitrary numeric priorities**<span style="white-space: pre-wrap;"> rather than a small fixed bucket range</span>
- <span style="white-space: pre-wrap;">you need </span>**blocking pop**<span style="white-space: pre-wrap;"> behavior built into the module</span>
- you need stable ordering across different priorities based on timestamp or insertion order

<span style="white-space: pre-wrap;">The implementation is designed around </span>**multiple producers, single consumer**.

## High-Level Design

The queue is made from:

- an array of FreeRTOS queue handles
- a count of how many buckets exist
- a 32-bit bitmap tracking which buckets are currently believed to be non-empty
- an optional task handle to notify when something is pushed

### Core idea

Each priority level has its own FreeRTOS queue.

The module maintains a bitmap:

- <span style="white-space: pre-wrap;">bit </span>`<span class="editor-theme-code">n</span>`<span style="white-space: pre-wrap;"> set = bucket </span>`<span class="editor-theme-code">n</span>`<span style="white-space: pre-wrap;"> is believed to contain at least one item</span>
- <span style="white-space: pre-wrap;">bit </span>`<span class="editor-theme-code">n</span>`<span style="white-space: pre-wrap;"> clear = bucket </span>`<span class="editor-theme-code">n</span>`<span style="white-space: pre-wrap;"> is believed to be empty</span>

This bitmap lets the consumer avoid blindly probing every queue all the time.

### Example

If there are 4 buckets:

- bucket 0 = low
- bucket 1 = medium
- bucket 2 = high
- bucket 3 = critical

And the bitmap is:

```c
non_empty_mask = 0b1010
```

then bucket 1 and bucket 3 are non-empty.

<span style="white-space: pre-wrap;">A call to </span>`<span class="editor-theme-code">bucketed_pqueue_pop()</span>`<span style="white-space: pre-wrap;"> will check from highest to lowest, so it will try:</span>

1. bucket 3
2. bucket 2
3. bucket 1
4. bucket 0

and return the first available item it finds.

## Data Structure

```c
typedef struct {  
  QueueHandle_t *buckets;  
  uint8_t num_buckets;  
  uint32_t non_empty_mask;  
  TaskHandle_t notifier;
} bucketed_pqueue_t;
```

### Fields

#### `<span class="editor-theme-code">buckets</span>`

<span style="white-space: pre-wrap;">Pointer to an array of </span>`<span class="editor-theme-code">QueueHandle_t</span>`.

Each entry is a FreeRTOS queue representing one priority bucket.

<p class="callout info"><span style="white-space: pre-wrap;">This array is </span>**not owned**<span style="white-space: pre-wrap;"> by the module. The caller must create the queues and ensure the array remains valid for the entire lifetime of the priority queue.</span></p>

#### `<span class="editor-theme-code">num_buckets</span>`

<span style="white-space: pre-wrap;">Number of buckets in the </span>`<span class="editor-theme-code">buckets</span>`<span style="white-space: pre-wrap;"> array.</span>

<span style="white-space: pre-wrap;">Valid range: </span>**1 to 32**.

<span style="white-space: pre-wrap;">The upper limit exists because </span>`<span class="editor-theme-code">non_empty_mask</span>`<span style="white-space: pre-wrap;"> is a 32-bit bitmap.</span>

#### `<span class="editor-theme-code">non_empty_mask</span>`

Bitmap used as a fast summary of which buckets contain items.

- <span style="white-space: pre-wrap;">bit </span>`<span class="editor-theme-code">0</span>`<span style="white-space: pre-wrap;"> corresponds to bucket </span>`<span class="editor-theme-code">0</span>`
- <span style="white-space: pre-wrap;">bit </span>`<span class="editor-theme-code">1</span>`<span style="white-space: pre-wrap;"> corresponds to bucket </span>`<span class="editor-theme-code">1</span>`
- etc.

This bitmap is updated on push, and cleared when the consumer determines a bucket is empty.

#### `<span class="editor-theme-code">notifier</span>`

Optional task to notify when an item is successfully pushed.

<span style="white-space: pre-wrap;">If not </span>`<span class="editor-theme-code">NULL</span>`, a push sets the notification bit:

```c
1UL << prio
```

in the target task’s notification value.

This is useful when the consumer task waits on task notifications instead of polling.

## Important behavioral guarantees

This module provides the following behavior:

### Strict priority

Higher-priority buckets are always preferred over lower-priority buckets.

<span style="white-space: pre-wrap;">If bucket 3 and bucket 1 both contain items, </span>`<span class="editor-theme-code">pop()</span>`<span style="white-space: pre-wrap;"> will always return from bucket 3 first.</span>

### FIFO within one priority

Because each bucket is a FreeRTOS queue, items in the same bucket are processed in insertion order.

### Non-blocking pop/peek

`<span class="editor-theme-code">pop()</span>`<span style="white-space: pre-wrap;"> and </span>`<span class="editor-theme-code">peek()</span>`<span style="white-space: pre-wrap;"> do not wait. If no item is available, they return </span>`<span class="editor-theme-code">RESULT_ERR_NOT_FOUND</span>`.

### Multi-context producers

There are separate APIs for:

- <span style="white-space: pre-wrap;">task context: </span>`<span class="editor-theme-code">bucketed_pqueue_push()</span>`
- <span style="white-space: pre-wrap;">ISR context: </span>`<span class="editor-theme-code">bucketed_pqueue_push_from_isr()</span>`

### Single-consumer design

<span style="white-space: pre-wrap;">The implementation assumes only one consumer performs </span>`<span class="editor-theme-code">pop()</span>`<span style="white-space: pre-wrap;"> and </span>`<span class="editor-theme-code">peek()</span>`.

That is not just a suggestion. It is a design constraint.

## Required setup

<span style="white-space: pre-wrap;">This module does </span>**not**<span style="white-space: pre-wrap;"> create FreeRTOS queues itself.</span>

The caller must:

1. create one FreeRTOS queue for each priority level
2. store those queue handles in an array
3. <span style="white-space: pre-wrap;">initialize a </span>`<span class="editor-theme-code">bucketed_pqueue_t</span>`<span style="white-space: pre-wrap;"> using that array</span>

### Example setup

```c
#define NUM_BUCKETS 4

static QueueHandle_t bucket_handles[NUM_BUCKETS];

bucketed_pqueue_t pq;

void app_init(void) {
  bucket_handles[0] = xQueueCreate(8, sizeof(my_msg_t));
  bucket_handles[1] = xQueueCreate(8, sizeof(my_msg_t));
  bucket_handles[2] = xQueueCreate(8, sizeof(my_msg_t));
  bucket_handles[3] = xQueueCreate(8, sizeof(my_msg_t));

  bucketed_pqueue_init(&pq, bucket_handles, NUM_BUCKETS, consumer_task_handle);
}
```

### Strong recommendation

<p class="callout info"><span style="white-space: pre-wrap;">All buckets should use the </span>**same item type or at least size**.</p>

<span style="white-space: pre-wrap;">Technically the module does not enforce that. Practically, mixing queue item sizes makes the API awkward and error-prone, because </span>`<span class="editor-theme-code">pop()</span>`<span style="white-space: pre-wrap;"> and </span>`<span class="editor-theme-code">peek()</span>`<span style="white-space: pre-wrap;"> write into a single </span>`<span class="editor-theme-code">out</span>`<span style="white-space: pre-wrap;"> buffer and the caller has no type information at that point.</span>

## Usage model

### Producer side

A producer decides the priority and pushes into the matching bucket.

Example:

```c
my_msg_t msg = { ... };
bucketed_pqueue_push(&pq, PRIORITY_HIGH, &msg, pdMS_TO_TICKS(10));
```

From an ISR:

```c
BaseType_t higher_woken = pdFALSE;
my_msg_t msg = { ... };

bucketed_pqueue_push_from_isr(&pq, PRIORITY_HIGH, &msg, &higher_woken);
portYIELD_FROM_ISR(higher_woken);
```

### Consumer side

The consumer repeatedly pops the highest-priority item available.

Example:

```c
my_msg_t msg;

while (bucketed_pqueue_pop(&pq, &msg) == RESULT_OK) {
  process_msg(&msg);
}
```

<span style="white-space: pre-wrap;">Because </span>`<span class="editor-theme-code">pop()</span>`<span style="white-space: pre-wrap;"> is non-blocking, a typical design is:</span>

1. consumer task blocks on a task notification
2. <span style="white-space: pre-wrap;">on wakeup, it calls </span>`<span class="editor-theme-code">pop()</span>`<span style="white-space: pre-wrap;"> in a loop until </span>`<span class="editor-theme-code">RESULT_ERR_NOT_FOUND</span>`

Example pattern:

```c
for (;;) {
  uint32_t notified_bits = 0;
  xTaskNotifyWait(0, UINT32_MAX, &notified_bits, portMAX_DELAY);

  my_msg_t msg;
  while (bucketed_pqueue_pop(&pq, &msg) == RESULT_OK) {
    process_msg(&msg);
  }
}
```

<span style="white-space: pre-wrap;">This pattern works well with the module’s </span>`<span class="editor-theme-code">notifier</span>`<span style="white-space: pre-wrap;"> mechanism.</span>

## Notification behavior

<span style="white-space: pre-wrap;">If </span>`<span class="editor-theme-code">notifier</span>`<span style="white-space: pre-wrap;"> is provided during initialization, every successful push sends a task notification with:</span>

```c
1UL << prio
```

<span style="white-space: pre-wrap;">using </span>`<span class="editor-theme-code">eSetBits</span>`.

This means:

- <span style="white-space: pre-wrap;">pushes do </span>**not overwrite**<span style="white-space: pre-wrap;"> previous notification bits</span>
- multiple buckets can be represented in the task notification value
- repeated pushes to the same priority keep the same bit set

### What the notification means

The notification indicates that at least one push occurred into that bucket.

<span style="white-space: pre-wrap;">It does </span>**not**<span style="white-space: pre-wrap;"> guarantee:</span>

- how many items are in that bucket
- that the bucket is still non-empty by the time the task wakes
- that the bitmap and queues are perfectly synchronized at all times

It is a wakeup hint, not a count.

<span style="white-space: pre-wrap;">That is fine. The consumer should drain the queue with repeated </span>`<span class="editor-theme-code">pop()</span>`<span style="white-space: pre-wrap;"> calls rather than assuming one notification equals one item.</span>

## Concurrency model and assumptions

This section matters more than the function list.

### Supported access pattern

#### Supported

- multiple producer tasks
- ISR producers
- <span style="white-space: pre-wrap;">one consumer task calling </span>`<span class="editor-theme-code">pop()</span>`<span style="white-space: pre-wrap;"> and/or </span>`<span class="editor-theme-code">peek()</span>`

#### Not supported

- <span style="white-space: pre-wrap;">multiple concurrent consumers calling </span>`<span class="editor-theme-code">pop()</span>`
- one task peeking while another task pops in a way that assumes strong synchronization guarantees
- external code modifying the underlying bucket queues directly behind this module’s back

<p class="callout danger"><span style="white-space: pre-wrap;">The module uses a bitmap plus queue operations, but it does </span>**not**<span style="white-space: pre-wrap;"> implement a full multi-consumer synchronization scheme around dequeue behavior.</span></p>

### Why single-consumer matters

The bitmap is read, scanned, and repaired in steps.

That is acceptable with one consumer, because any race is limited to producer updates and queue state changes, and the consumer can repair stale bits safely.

With multiple consumers, two tasks could:

- observe the same bitmap snapshot
- both decide the same bucket has data
- one drains the queue
- the other sees an empty queue and clears the bit

That alone is survivable, but once multiple consumers are simultaneously probing and repairing, reasoning about ordering and fairness gets messy fast. This module avoids that entire circus by assuming a single consumer.

### Critical sections

The bitmap is protected with FreeRTOS critical sections:

- `<span class="editor-theme-code">taskENTER_CRITICAL()</span>`<span style="white-space: pre-wrap;"> / </span>`<span class="editor-theme-code">taskEXIT_CRITICAL()</span>`
- `<span class="editor-theme-code">taskENTER_CRITICAL_FROM_ISR()</span>`<span style="white-space: pre-wrap;"> / </span>`<span class="editor-theme-code">taskEXIT_CRITICAL_FROM_ISR()</span>`

<span style="white-space: pre-wrap;">These critical sections protect </span>**bitmap access**, not the whole queue operation sequence.

That means queue operations and bitmap updates are not one indivisible transaction.

<span style="white-space: pre-wrap;">This is intentional and mostly fine for the intended model, but maintainers should understand that the bitmap is a </span>**best-effort summary**, not a perfect mirror of queue state.

## Known implementation characteristics

### Priority scan is linear in number of buckets

`<span class="editor-theme-code">pop()</span>`<span style="white-space: pre-wrap;"> and </span>`<span class="editor-theme-code">peek()</span>`<span style="white-space: pre-wrap;"> scan from highest to lowest priority:</span>

```c
for (int prio = num_buckets - 1; prio >= 0; prio--)
```

<span style="white-space: pre-wrap;">So the dequeue cost is </span>`<span class="editor-theme-code">O(num_buckets)</span>`<span style="white-space: pre-wrap;"> in the worst case.</span>

<span style="white-space: pre-wrap;">Since </span>`<span class="editor-theme-code">num_buckets <= 32</span>`, this is usually acceptable in embedded systems.

<p class="callout warning">If someone later decides to turn this into 128 priorities with a 32-bit bitmap, you might want to consider a better scanning system for all buckets.</p>

That said, the loop is incredibly efficient doing only a bit-wise check to know if the bucket is populated or not.

### Bitmap may be temporarily stale

The module can have these transient states:

- queue contains data but bitmap not yet updated
- bitmap says non-empty but queue is empty

<span style="white-space: pre-wrap;">The code handles the second case explicitly by clearing stale bits when </span>`<span class="editor-theme-code">xQueueReceive()</span>`<span style="white-space: pre-wrap;"> or </span>`<span class="editor-theme-code">xQueuePeek()</span>`<span style="white-space: pre-wrap;"> fails.</span>

The first case is shorter-lived and happens between a successful queue send and the bitmap update, or if initialization starts with pre-filled queues.

This is why the bitmap should be viewed as a hint structure.

### No ownership of queue storage

<p class="callout info">The module does not allocate or destroy the bucket queues.</p>

It only stores the queue handles provided by the caller.

The caller is responsible for:

- creating queues before initialization
- keeping them alive while the priority queue is in use
- ensuring item types and queue lengths are appropriate

### No reset/deinit API

There is no deinitialization function.

If needed, the caller must manage queue lifecycle itself.

If a reset feature is ever added, it must consider:

- draining or recreating each bucket
- <span style="white-space: pre-wrap;">resetting </span>`<span class="editor-theme-code">non_empty_mask</span>`
- possible interaction with producers still running

## Practical example

### Scenario

A system has three priorities:

- `<span class="editor-theme-code">0</span>`: telemetry
- `<span class="editor-theme-code">1</span>`: commands
- `<span class="editor-theme-code">2</span>`: emergency actions

All use the same message type:

```c
typedef struct {
  uint8_t type;
  uint32_t value;
} app_msg_t;
```

### Setup

```c
#define APP_NUM_PRIORITIES 3

static QueueHandle_t app_buckets[APP_NUM_PRIORITIES];
static bucketed_pqueue_t app_pq;

void app_queue_init(TaskHandle_t consumer_task) {
  app_buckets[0] = xQueueCreate(16, sizeof(app_msg_t));
  app_buckets[1] = xQueueCreate(16, sizeof(app_msg_t));
  app_buckets[2] = xQueueCreate(8, sizeof(app_msg_t));

  bucketed_pqueue_init(&app_pq, app_buckets, APP_NUM_PRIORITIES, consumer_task);
}
```

### Producer task

```c
void send_command(uint32_t cmd) {
  app_msg_t msg = {
    .type = 1,
    .value = cmd,
  };

  (void)bucketed_pqueue_push(&app_pq, 1, &msg, 0);
}
```

### ISR producer

```c
void emergency_isr(void) {
  BaseType_t higher_woken = pdFALSE;

  app_msg_t msg = {
    .type = 2,
    .value = 0xDEADU,
  };

  (void)bucketed_pqueue_push_from_isr(&app_pq, 2, &msg, &higher_woken);
  portYIELD_FROM_ISR(higher_woken);
}
```

### Consumer task

```c
void consumer_task(void *arg) {
  (void)arg;

  for (;;) {
    uint32_t notify_bits;
    xTaskNotifyWait(0, UINT32_MAX, &notify_bits, portMAX_DELAY);

    app_msg_t msg;
    while (bucketed_pqueue_pop(&app_pq, &msg) == RESULT_OK) {
      handle_message(&msg);
    }
  }
}
```

### [Result](https://bookstack.roboteamtwente.nl/books/embedded-infastructure/page/result-library "Result Library")

If telemetry, commands, and emergency messages all arrive, the consumer will process:

1. emergency
2. commands
3. telemetry

Within each class, messages remain FIFO.

## Error handling

<span style="white-space: pre-wrap;">The module uses </span>`<span class="editor-theme-code">result_t</span>`, which is defined elsewhere.

Based on the implementation, these results are used:

### `<span class="editor-theme-code">RESULT_OK</span>`

Operation succeeded.

### `<span class="editor-theme-code">RESULT_ERR_INVALID_ARG</span>`

Returned when the caller passes invalid arguments, such as null pointers or out-of-range priority.

### `<span class="editor-theme-code">RESULT_ERR_OVERFLOW</span>`

Returned by push functions when the selected bucket queue cannot accept the item.

This usually means the bucket queue is full, or the send timed out.

### `<span class="editor-theme-code">RESULT_ERR_NOT_FOUND</span>`

<span style="white-space: pre-wrap;">Returned by </span>`<span class="editor-theme-code">pop()</span>`<span style="white-space: pre-wrap;"> or </span>`<span class="editor-theme-code">peek()</span>`<span style="white-space: pre-wrap;"> when no item is available.</span>

This is not necessarily an error in the usual sense. It is the normal result for an empty priority queue.

## Maintenance notes

### If you change the number of buckets beyond 32

You must also change:

- the bitmap type
- all bit shift logic
- input validation
- <span style="white-space: pre-wrap;">notification assumptions based on </span>`<span class="editor-theme-code">1UL << prio</span>`

<span style="white-space: pre-wrap;">Right now, 32 buckets is a </span>**hard architectural limit**.

### If you add blocking pop behavior

Be careful.

A naive implementation that blocks on each bucket in order would break strict priority or become awkward and expensive.

A better approach is usually to keep the existing design:

- producers notify a task
- consumer waits on notification
- <span style="white-space: pre-wrap;">consumer drains with non-blocking </span>`<span class="editor-theme-code">pop()</span>`

If you still add a blocking API, document its wakeup semantics very clearly.

### If you want multiple consumers

This requires redesign.

You would need to revisit:

- bitmap synchronization
- dequeue race handling
- <span style="white-space: pre-wrap;">semantics of </span>`<span class="editor-theme-code">peek()</span>`
- fairness between consumers
- whether the notifier model still makes sense

Do not label it “thread-safe” just because critical sections exist.

---

### If different buckets need different item types

The current API is not a good fit for that.

`<span class="editor-theme-code">pop()</span>`<span style="white-space: pre-wrap;"> and </span>`<span class="editor-theme-code">peek()</span>`<span style="white-space: pre-wrap;"> return into one generic </span>`<span class="editor-theme-code">out</span>`<span style="white-space: pre-wrap;"> buffer with no explicit type metadata.</span>

If you need heterogeneous payloads, safer patterns are:

- use a tagged union message type
- store pointers to separately managed objects
- wrap payloads in a common envelope struct

### If queues are pre-filled before init

<span style="white-space: pre-wrap;">The bitmap starts at zero during </span>`<span class="editor-theme-code">bucketed_pqueue_init()</span>`.

So pre-filled queues will not be visible until something later sets the relevant bits, or until code is changed to rebuild the bitmap.

<span style="white-space: pre-wrap;">If supporting pre-filled queues matters, one possible improvement is for </span>`<span class="editor-theme-code">init()</span>`<span style="white-space: pre-wrap;"> to inspect each bucket using </span>`<span class="editor-theme-code">uxQueueMessagesWaiting()</span>`<span style="white-space: pre-wrap;"> and initialize </span>`<span class="editor-theme-code">non_empty_mask</span>`<span style="white-space: pre-wrap;"> accordingly.</span>

<span style="white-space: pre-wrap;">That behavior does </span>**not**<span style="white-space: pre-wrap;"> exist today.</span>