Skip to content

Commit d3b65e0

Browse files
committed
Fix read_last could read unpublished data; Added shm init handshake; bump version number to 1.4.0
1 parent b97722c commit d3b65e0

File tree

9 files changed

+295
-44
lines changed

9 files changed

+295
-44
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
# Escape dots in version for regex
2929
VERSION_ESCAPED=$(echo "$VERSION" | sed 's/\./\\./g')
3030
# Use awk to extract content: start at version header, stop at next ## header
31-
CHANGES=$(awk "BEGIN{p=0} /^# $VERSION_ESCAPED/{p=1;next} /^# /{p=0} p" CHANGELOG | sed '/^$/d')
31+
CHANGES=$(awk "BEGIN{p=0} /^# $VERSION_ESCAPED( |$)/{p=1;next} /^# /{p=0} p" CHANGELOG | sed '/^$/d')
3232
3333
if [ -z "$CHANGES" ]; then
3434
echo "CHANGELOG_CONTENT=No changelog entry found for this version." >> $GITHUB_OUTPUT

CHANGELOG

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
# v1.4.0 - 2026-02-04
2+
- **BREAKING CHANGE**: Added last_published_index and header magic in shared memory header
3+
- Fixed shared-memory attach race by adding an init-state handshake with readiness wait and legacy fallback
4+
- Added last-published tracking so `read_last()` only returns published data; added a regression test
5+
- Changed read_last() return signature to std::pair<T*, uint32_t>
6+
- Updated `read_last()` documentation for legacy shared-memory segments
7+
- Relaxed memory ordering in the shared atomic cursor path for lower overhead
8+
- Fixed release workflow changelog extraction to match version headers with trailing dates
9+
110
# v1.3.0 - 2026-01-30
211
- **BREAKING CHANGE**: Replaced platform-specific shared memory implementation with slick-shm library
312
- Removed all Windows-specific code (CreateFileMapping, MapViewOfFile, etc.)

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
cmake_minimum_required(VERSION 3.10)
22

33
project(slick-queue
4-
VERSION 1.3.0
4+
VERSION 1.4.0
55
DESCRIPTION "A C++ Lock-Free MPMC queue"
66
LANGUAGES CXX)
77

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2020-2026 Slick Quant LLC.
3+
Copyright (c) 2020-2026 Slick Quant LLC
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ SlickQueue(const char* shm_name); // Reader/Attacher
212212
- `void publish(uint64_t slot, uint32_t n = 1)` - Publish `n` written items to consumers
213213
- `std::pair<T*, uint32_t> read(uint64_t& cursor)` - Read next available item (independent cursor)
214214
- `std::pair<T*, uint32_t> read(std::atomic<uint64_t>& cursor)` - Read next available item (shared atomic cursor for work-stealing)
215-
- `T* read_last()` - Read the most recently published item without a cursor
215+
- `std::pair<T*, uint32_t> read_last()` - Read the most recently published item without a cursor
216216
- `uint32_t size()` - Get queue capacity
217217
- `uint64_t loss_count() const` - Get count of skipped items due to overwrite (debug-only if enabled)
218218
- `void reset()` - Reset the queue, invalidating all existing data
@@ -227,10 +227,10 @@ SlickQueue(const char* shm_name); // Reader/Attacher
227227
228228
**CPU Relax Backoff**: Define `SLICK_QUEUE_ENABLE_CPU_RELAX=0` to disable the pause/yield backoff used on contended CAS loops (default is enabled). Disabling may reduce latency in very short contention bursts but can increase CPU usage under load.
229229
230-
**⚠️ Reserve Size Limitation**: When using `read_last()`, the number of slots in any `reserve(n)` call **must not exceed 65,535** (2^16 - 1). This is because the size is stored in 16 bits within the packed atomic.
230+
**⚠️ Reserve Size Limitation (legacy shared memory)**: Older shared-memory segments used the 16-bit size stored in the packed reservation atomic to compute `read_last()`. New segments track the last published index separately, so this limit no longer applies in normal use.
231231
232+
- If you attach to a shared-memory segment created by older versions, keep `reserve(n) <= 65,535` when using `read_last()`
232233
- For typical use cases with `reserve()` or `reserve(1)`, this limit is not a concern
233-
- If you need to reserve more than 65,535 slots at once, do not use `read_last()`
234234
- The 48-bit index supports up to 2^48 (281 trillion) iterations, sufficient for any practical application
235235
236236
## Performance Characteristics

include/slick/queue.h

Lines changed: 136 additions & 37 deletions
Large diffs are not rendered by default.

tests/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ if(NOT GTest_FOUND)
1313
endif()
1414

1515
add_executable(slick-queue-tests tests.cpp shm_tests.cpp)
16+
if(MSVC)
17+
add_compile_options(/wd4996)
18+
endif()
1619

1720
target_link_libraries(slick-queue-tests PRIVATE slick::queue GTest::gtest_main)
1821

tests/shm_tests.cpp

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,84 @@ TEST(ShmTests, SizeMismatch) {
202202
}
203203
}, std::runtime_error);
204204
}
205+
206+
TEST(ShmTests, ReadLastUsesLatestReserveSize) {
207+
SlickQueue<int> queue(8, "sq_read_last");
208+
SlickQueue<int> reader_queue(8, "sq_read_last");
209+
210+
auto first = queue.reserve(2);
211+
*queue[first] = 1;
212+
*queue[first + 1] = 2;
213+
queue.publish(first, 2);
214+
215+
auto last = queue.reserve(1);
216+
*queue[last] = 3;
217+
queue.publish(last, 1);
218+
219+
auto [latest, size] = reader_queue.read_last();
220+
ASSERT_NE(latest, nullptr);
221+
EXPECT_EQ(*latest, 3);
222+
EXPECT_EQ(size, 1);
223+
}
224+
225+
TEST(ShmTests, ReadLastIgnoresUnpublishedReservation) {
226+
SlickQueue<int> queue(8, "sq_read_last2");
227+
SlickQueue<int> reader_queue(8, "sq_read_last2");
228+
229+
auto first = queue.reserve(2);
230+
*queue[first] = 1;
231+
*queue[first + 1] = 2;
232+
queue.publish(first, 2);
233+
234+
auto last = queue.reserve(1);
235+
*queue[last] = 3;
236+
237+
auto [latest, size] = reader_queue.read_last();
238+
ASSERT_NE(latest, nullptr);
239+
EXPECT_EQ(*latest, 1);
240+
EXPECT_EQ(size, 2);
241+
}
242+
243+
TEST(ShmTests, ReadLastUsesLatestReserveSizeMultiple) {
244+
SlickQueue<char> queue(256, "sq_read_last_multi");
245+
SlickQueue<char> reader_queue(256, "sq_read_last_multi");
246+
247+
const char* first_str = "One";
248+
uint32_t length = static_cast<uint32_t>(std::strlen(first_str) + 1);
249+
auto first = queue.reserve(length);
250+
std::strcpy(queue[first], first_str);
251+
queue.publish(first, length);
252+
253+
const char* last_str = "Four";
254+
length = static_cast<uint32_t>(strlen(first_str) + 1);
255+
auto last = queue.reserve(length);
256+
std::strcpy(queue[last], last_str);
257+
queue.publish(last, length);
258+
259+
auto [latest, size] = reader_queue.read_last();
260+
ASSERT_NE(latest, nullptr);
261+
std::string s(latest, size);
262+
EXPECT_EQ(strncmp(latest, last_str, size), 0);
263+
}
264+
265+
TEST(ShmTests, ReadLastIgnoresUnpublishedReservationMultiple) {
266+
SlickQueue<char> queue(256, "sq_read_last_multi2");
267+
SlickQueue<char> reader_queue(256, "sq_read_last_multi2");
268+
269+
const char* first_str = "One";
270+
uint32_t length = static_cast<uint32_t>(std::strlen(first_str) + 1);
271+
auto first = queue.reserve(length);
272+
std::strcpy(queue[first], first_str);
273+
queue.publish(first, length);
274+
275+
const char* last_str = "Four";
276+
length = static_cast<uint32_t>(strlen(first_str) + 1);
277+
auto last = queue.reserve(length);
278+
std::strcpy(queue[last], last_str);
279+
280+
auto [latest, size] = reader_queue.read_last();
281+
ASSERT_NE(latest, nullptr);
282+
std::string s(latest, size);
283+
EXPECT_EQ(strncmp(latest, first_str, size), 0);
284+
}
285+

tests/tests.cpp

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,68 @@ TEST(SlickQueueTests, ReadLastUsesLatestReserveSize) {
137137
*queue[last] = 3;
138138
queue.publish(last, 1);
139139

140-
auto latest = queue.read_last();
140+
auto [latest, size] = queue.read_last();
141141
ASSERT_NE(latest, nullptr);
142142
EXPECT_EQ(*latest, 3);
143+
EXPECT_EQ(size, 1);
144+
}
145+
146+
TEST(SlickQueueTests, ReadLastIgnoresUnpublishedReservation) {
147+
SlickQueue<int> queue(8);
148+
149+
auto first = queue.reserve(2);
150+
*queue[first] = 1;
151+
*queue[first + 1] = 2;
152+
queue.publish(first, 2);
153+
154+
auto last = queue.reserve(1);
155+
*queue[last] = 3;
156+
157+
auto [latest, size] = queue.read_last();
158+
ASSERT_NE(latest, nullptr);
159+
EXPECT_EQ(*latest, 1);
160+
EXPECT_EQ(size, 2);
161+
}
162+
163+
TEST(SlickQueueTests, ReadLastUsesLatestReserveSizeMultiple) {
164+
SlickQueue<char> queue(256);
165+
166+
const char* first_str = "One";
167+
uint32_t length = static_cast<uint32_t>(std::strlen(first_str) + 1);
168+
auto first = queue.reserve(length);
169+
std::strcpy(queue[first], first_str);
170+
queue.publish(first, length);
171+
172+
const char* last_str = "Four";
173+
length = static_cast<uint32_t>(strlen(first_str) + 1);
174+
auto last = queue.reserve(length);
175+
std::strcpy(queue[last], last_str);
176+
queue.publish(last, length);
177+
178+
auto [latest, size] = queue.read_last();
179+
ASSERT_NE(latest, nullptr);
180+
std::string s(latest, size);
181+
EXPECT_EQ(strncmp(latest, last_str, size), 0);
182+
}
183+
184+
TEST(SlickQueueTests, ReadLastIgnoresUnpublishedReservationMultiple) {
185+
SlickQueue<char> queue(256);
186+
187+
const char* first_str = "One";
188+
uint32_t length = static_cast<uint32_t>(std::strlen(first_str) + 1);
189+
auto first = queue.reserve(length);
190+
std::strcpy(queue[first], first_str);
191+
queue.publish(first, length);
192+
193+
const char* last_str = "Four";
194+
length = static_cast<uint32_t>(strlen(first_str) + 1);
195+
auto last = queue.reserve(length);
196+
std::strcpy(queue[last], last_str);
197+
198+
auto [latest, size] = queue.read_last();
199+
ASSERT_NE(latest, nullptr);
200+
std::string s(latest, size);
201+
EXPECT_EQ(strncmp(latest, first_str, size), 0);
143202
}
144203

145204
TEST(SlickQueueTests, LossyOverwriteSkipsOldData) {

0 commit comments

Comments
 (0)