Closes #11
This commit is contained in:
74
analysis/05-Message_Queue_Partitioning/example/message_queue_partitioning/.gitignore
vendored
Normal file
74
analysis/05-Message_Queue_Partitioning/example/message_queue_partitioning/.gitignore
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# This file is used to ignore files which are generated
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
*~
|
||||||
|
*.autosave
|
||||||
|
*.a
|
||||||
|
*.core
|
||||||
|
*.moc
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.orig
|
||||||
|
*.rej
|
||||||
|
*.so
|
||||||
|
*.so.*
|
||||||
|
*_pch.h.cpp
|
||||||
|
*_resource.rc
|
||||||
|
*.qm
|
||||||
|
.#*
|
||||||
|
*.*#
|
||||||
|
core
|
||||||
|
!core/
|
||||||
|
tags
|
||||||
|
.DS_Store
|
||||||
|
.directory
|
||||||
|
*.debug
|
||||||
|
Makefile*
|
||||||
|
*.prl
|
||||||
|
*.app
|
||||||
|
moc_*.cpp
|
||||||
|
ui_*.h
|
||||||
|
qrc_*.cpp
|
||||||
|
Thumbs.db
|
||||||
|
*.res
|
||||||
|
*.rc
|
||||||
|
/.qmake.cache
|
||||||
|
/.qmake.stash
|
||||||
|
|
||||||
|
# qtcreator generated files
|
||||||
|
*.pro.user*
|
||||||
|
CMakeLists.txt.user*
|
||||||
|
|
||||||
|
# xemacs temporary files
|
||||||
|
*.flc
|
||||||
|
|
||||||
|
# Vim temporary files
|
||||||
|
.*.swp
|
||||||
|
|
||||||
|
# Visual Studio generated files
|
||||||
|
*.ib_pdb_index
|
||||||
|
*.idb
|
||||||
|
*.ilk
|
||||||
|
*.pdb
|
||||||
|
*.sln
|
||||||
|
*.suo
|
||||||
|
*.vcproj
|
||||||
|
*vcproj.*.*.user
|
||||||
|
*.ncb
|
||||||
|
*.sdf
|
||||||
|
*.opensdf
|
||||||
|
*.vcxproj
|
||||||
|
*vcxproj.*
|
||||||
|
|
||||||
|
# MinGW generated files
|
||||||
|
*.Debug
|
||||||
|
*.Release
|
||||||
|
|
||||||
|
# Python byte code
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Binaries
|
||||||
|
# --------
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
#include <cassert>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
struct Message {
|
||||||
|
std::uint32_t id;
|
||||||
|
bool retry;
|
||||||
|
Message *next;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MessageQueue {
|
||||||
|
Message *head;
|
||||||
|
Message *tail;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PartitionResult {
|
||||||
|
MessageQueue ready;
|
||||||
|
MessageQueue retry;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void append (MessageQueue &queue, Message *message) {
|
||||||
|
assert (message != nullptr);
|
||||||
|
assert (message->next == nullptr);
|
||||||
|
|
||||||
|
if (queue.tail == nullptr) {
|
||||||
|
queue.head = message;
|
||||||
|
queue.tail = message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
queue.tail->next = message;
|
||||||
|
queue.tail = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
PartitionResult partition_messages (MessageQueue &source) {
|
||||||
|
PartitionResult result{
|
||||||
|
{nullptr, nullptr},
|
||||||
|
{nullptr, nullptr}
|
||||||
|
};
|
||||||
|
|
||||||
|
Message *current = source.head;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The source queue is consumed by this operation.
|
||||||
|
*
|
||||||
|
* Clearing it before traversal makes the ownership transfer explicit:
|
||||||
|
* every node taken from the original queue must be appended to exactly
|
||||||
|
* one of the two result queues.
|
||||||
|
*/
|
||||||
|
source.head = nullptr;
|
||||||
|
source.tail = nullptr;
|
||||||
|
|
||||||
|
while (current != nullptr) {
|
||||||
|
/*
|
||||||
|
* Save the traversal link before modifying current->next.
|
||||||
|
* The same intrusive link is reused by the destination queue.
|
||||||
|
*/
|
||||||
|
Message *next = current->next;
|
||||||
|
current->next = nullptr;
|
||||||
|
|
||||||
|
if (current->retry)
|
||||||
|
append (result.retry, current);
|
||||||
|
else
|
||||||
|
append (result.ready, current);
|
||||||
|
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_queue (const char *name, const MessageQueue &queue) {
|
||||||
|
std::cout << name << ": ";
|
||||||
|
|
||||||
|
const Message *current = queue.head;
|
||||||
|
|
||||||
|
if (current == nullptr) {
|
||||||
|
std::cout << "<empty>\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (current != nullptr) {
|
||||||
|
std::cout << current->id;
|
||||||
|
|
||||||
|
if (current->next != nullptr)
|
||||||
|
std::cout << " -> ";
|
||||||
|
|
||||||
|
current = current->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::size_t queue_size (const MessageQueue &queue) {
|
||||||
|
std::size_t size = 0;
|
||||||
|
const Message *current = queue.head;
|
||||||
|
|
||||||
|
while (current != nullptr) {
|
||||||
|
++size;
|
||||||
|
current = current->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void verify_queue (const MessageQueue &queue) {
|
||||||
|
if (queue.head == nullptr) {
|
||||||
|
assert (queue.tail == nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert (queue.tail != nullptr);
|
||||||
|
assert (queue.tail->next == nullptr);
|
||||||
|
|
||||||
|
const Message *current = queue.head;
|
||||||
|
|
||||||
|
while (current->next != nullptr)
|
||||||
|
current = current->next;
|
||||||
|
|
||||||
|
assert (current == queue.tail);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
Message a{1U, false, nullptr};
|
||||||
|
Message b{2U, true, nullptr};
|
||||||
|
Message c{3U, false, nullptr};
|
||||||
|
Message d{4U, true, nullptr};
|
||||||
|
|
||||||
|
a.next = &b;
|
||||||
|
b.next = &c;
|
||||||
|
c.next = &d;
|
||||||
|
|
||||||
|
MessageQueue outgoing{&a, &d};
|
||||||
|
|
||||||
|
std::cout << "Before partition\n";
|
||||||
|
print_queue ("Outgoing", outgoing);
|
||||||
|
|
||||||
|
const PartitionResult result = partition_messages (outgoing);
|
||||||
|
|
||||||
|
std::cout << "\nAfter partition\n";
|
||||||
|
print_queue ("Outgoing", outgoing);
|
||||||
|
print_queue ("Ready", result.ready);
|
||||||
|
print_queue ("Retry", result.retry);
|
||||||
|
|
||||||
|
verify_queue (outgoing);
|
||||||
|
verify_queue (result.ready);
|
||||||
|
verify_queue (result.retry);
|
||||||
|
|
||||||
|
assert (outgoing.head == nullptr);
|
||||||
|
assert (outgoing.tail == nullptr);
|
||||||
|
|
||||||
|
assert (result.ready.head == &a);
|
||||||
|
assert (result.ready.tail == &c);
|
||||||
|
assert (a.next == &c);
|
||||||
|
assert (c.next == nullptr);
|
||||||
|
|
||||||
|
assert (result.retry.head == &b);
|
||||||
|
assert (result.retry.tail == &d);
|
||||||
|
assert (b.next == &d);
|
||||||
|
assert (d.next == nullptr);
|
||||||
|
|
||||||
|
assert (queue_size (result.ready) + queue_size (result.retry) == 4U);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
TEMPLATE = app
|
||||||
|
CONFIG += console c++17
|
||||||
|
CONFIG -= app_bundle
|
||||||
|
CONFIG -= qt
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
main.cpp
|
||||||
276
analysis/05-Message_Queue_Partitioning/readme.md
Normal file
276
analysis/05-Message_Queue_Partitioning/readme.md
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
# #05 — Message Queue Partitioning
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
A communication subsystem maintains a singly linked intrusive queue of outgoing messages.
|
||||||
|
|
||||||
|
Each message contains a transmission identifier, a retry flag, and a pointer to the next message:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct Message {
|
||||||
|
uint32_t id;
|
||||||
|
bool retry;
|
||||||
|
Message* next;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
After a transmission attempt, some messages may need to be retried.
|
||||||
|
|
||||||
|
Partition the original queue into two separate queues:
|
||||||
|
|
||||||
|
- the ready queue, containing messages that do not require another transmission attempt;
|
||||||
|
- the retry queue, containing messages marked for retry.
|
||||||
|
|
||||||
|
The relative order of messages must be preserved in both queues.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- no dynamic memory allocation;
|
||||||
|
- no copying of messages;
|
||||||
|
- reuse the existing list nodes;
|
||||||
|
- preserve the original order in both resulting queues;
|
||||||
|
- process the queue in `O(n)` time.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
### Input
|
||||||
|
|
||||||
|
```text
|
||||||
|
A -> B -> C -> D
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
A: retry = false
|
||||||
|
B: retry = true
|
||||||
|
C: retry = false
|
||||||
|
D: retry = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Result
|
||||||
|
|
||||||
|
Ready queue
|
||||||
|
|
||||||
|
```text
|
||||||
|
A -> C
|
||||||
|
```
|
||||||
|
|
||||||
|
Retry queue
|
||||||
|
|
||||||
|
```text
|
||||||
|
B -> D
|
||||||
|
```
|
||||||
|
|
||||||
|
The original queue is consumed during the operation, and every message must belong to exactly one of the two resulting queues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Analysis
|
||||||
|
|
||||||
|
At first glance, this looks like another linked list interview problem.
|
||||||
|
|
||||||
|
Traverse the list.
|
||||||
|
|
||||||
|
Check a flag.
|
||||||
|
|
||||||
|
Split the nodes into two lists.
|
||||||
|
|
||||||
|
Complexity: **O(n)**.
|
||||||
|
|
||||||
|
Simple.
|
||||||
|
|
||||||
|
Except this is one of those rare cases where the interview version is surprisingly close to a real engineering task.
|
||||||
|
|
||||||
|
The interesting part is not the algorithm.
|
||||||
|
|
||||||
|
The interesting part is what the algorithm is actually modifying.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## This Is Not About Two Lists
|
||||||
|
|
||||||
|
Each node already exists.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct Message {
|
||||||
|
uint32_t id;
|
||||||
|
bool retry;
|
||||||
|
Message* next;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
No objects are created.
|
||||||
|
|
||||||
|
No objects are destroyed.
|
||||||
|
|
||||||
|
No messages are copied.
|
||||||
|
|
||||||
|
Only ownership changes.
|
||||||
|
|
||||||
|
The original outgoing queue disappears and every message becomes part of exactly one new queue.
|
||||||
|
|
||||||
|
That small detail changes the entire nature of the problem.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Real Challenge
|
||||||
|
|
||||||
|
The boolean itself is trivial.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
retry == true
|
||||||
|
```
|
||||||
|
|
||||||
|
is simply a classification.
|
||||||
|
|
||||||
|
The difficult part is maintaining the integrity of two intrusive queues while consuming a third one.
|
||||||
|
|
||||||
|
Every processed node must satisfy one invariant:
|
||||||
|
|
||||||
|
- belong to exactly one queue;
|
||||||
|
- never be lost;
|
||||||
|
- never appear twice;
|
||||||
|
- never keep stale links into the original list.
|
||||||
|
|
||||||
|
Most bugs are not caused by the condition.
|
||||||
|
|
||||||
|
They are caused by pointer manipulation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why Saving `next` Matters
|
||||||
|
|
||||||
|
The same pointer is used for two completely different purposes.
|
||||||
|
|
||||||
|
During traversal:
|
||||||
|
|
||||||
|
```text
|
||||||
|
current -> next
|
||||||
|
```
|
||||||
|
|
||||||
|
is how we reach the remaining nodes.
|
||||||
|
|
||||||
|
After insertion into a new queue:
|
||||||
|
|
||||||
|
```text
|
||||||
|
current -> next
|
||||||
|
```
|
||||||
|
|
||||||
|
becomes part of another list.
|
||||||
|
|
||||||
|
If the original `next` pointer is overwritten before it is saved, the remainder of the queue is simply lost.
|
||||||
|
|
||||||
|
This is one of the classic pitfalls of intrusive containers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stable Partition
|
||||||
|
|
||||||
|
The requirements also say:
|
||||||
|
|
||||||
|
> preserve order
|
||||||
|
|
||||||
|
That sounds minor.
|
||||||
|
|
||||||
|
It isn't.
|
||||||
|
|
||||||
|
Appending to the head would produce:
|
||||||
|
|
||||||
|
```text
|
||||||
|
D -> B
|
||||||
|
```
|
||||||
|
|
||||||
|
instead of
|
||||||
|
|
||||||
|
```text
|
||||||
|
B -> D
|
||||||
|
```
|
||||||
|
|
||||||
|
The algorithm therefore performs a **stable partition**, preserving FIFO order in both resulting queues.
|
||||||
|
|
||||||
|
In a communication subsystem this is often essential because later messages may depend on earlier ones.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why No Allocation?
|
||||||
|
|
||||||
|
The requirement
|
||||||
|
|
||||||
|
```text
|
||||||
|
no allocation
|
||||||
|
```
|
||||||
|
|
||||||
|
isn't there to make the problem harder.
|
||||||
|
|
||||||
|
It reflects reality.
|
||||||
|
|
||||||
|
Communication stacks, embedded systems and real-time software often avoid dynamic allocation while processing packets or messages.
|
||||||
|
|
||||||
|
The messages already exist.
|
||||||
|
|
||||||
|
Only their position inside processing queues changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hidden Engineering Questions
|
||||||
|
|
||||||
|
The implementation itself is small.
|
||||||
|
|
||||||
|
The engineering questions are not.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
- Who owns the original queue after partitioning?
|
||||||
|
- Can another thread append messages during the operation?
|
||||||
|
- Can an interrupt modify the queue?
|
||||||
|
- What happens if the queue is already corrupted?
|
||||||
|
- Can a message belong to multiple intrusive containers?
|
||||||
|
- Should retry count also be updated?
|
||||||
|
- Is there exponential backoff before retrying?
|
||||||
|
|
||||||
|
None of these appear in the problem statement.
|
||||||
|
|
||||||
|
All of them appear in production systems.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What This Problem Actually Tests
|
||||||
|
|
||||||
|
Unlike many linked list exercises, this one evaluates something genuinely useful.
|
||||||
|
|
||||||
|
It tests whether a developer can safely manipulate ownership using pointers while preserving structural invariants.
|
||||||
|
|
||||||
|
The algorithm itself is almost secondary.
|
||||||
|
|
||||||
|
Correctness is everything.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Takeaway
|
||||||
|
|
||||||
|
This is one of the few interview-style linked list problems that has a direct equivalent in production software.
|
||||||
|
|
||||||
|
Not because splitting a list is inherently interesting.
|
||||||
|
|
||||||
|
But because communication stacks, schedulers, networking software and embedded systems continuously reorganize intrusive queues exactly like this.
|
||||||
|
|
||||||
|
The interview version removes most of the surrounding system.
|
||||||
|
|
||||||
|
The engineering version adds ownership, invariants, concurrency and failure handling.
|
||||||
|
|
||||||
|
The pointer operations remain almost identical.
|
||||||
|
|
||||||
|
The responsibility does not.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Perspective
|
||||||
|
|
||||||
|
> Exists in real engineering?
|
||||||
|
|
||||||
|
**Yes. Frequently.**
|
||||||
|
|
||||||
|
> Exists in interview form?
|
||||||
|
|
||||||
|
**Yes.**
|
||||||
|
|
||||||
|
One of the rare cases where the interview problem remains close to its real-world counterpart.
|
||||||
Reference in New Issue
Block a user