Fix/buffer size overloading - Fix SHM Buffer Over-Sizing and Condvar Hangs#108
Fix/buffer size overloading - Fix SHM Buffer Over-Sizing and Condvar Hangs#108
Conversation
dustinblack
left a comment
There was a problem hiding this comment.
PR #108 Review
Buffer sizing fix — looks good
The core change (fixed 64KB SHM buffer instead of sizing to fit all messages) is well-motivated and the before/after data is compelling. The buffer sizing logic in benchmark.rs and benchmark_blocking.rs is clean, the warn! → debug! downgrade makes sense, and the test coverage is thorough (boundary tests, large messages, duration mode, user override).
Condvar/polling changes must be removed
src/ipc/shared_memory_blocking.rs:286-432 (write path) and src/ipc/shared_memory_blocking.rs:437-598 (read path)
The pthread_cond_timedwait, broken-condvar detection, write_data_polling(), and read_data_polling() are the same container-IPC code that was removed from PR #104 at review (ba2e60c). The container-to-container IPC work is out of scope and will be re-architected in the future, so this code should not be introduced here.
Beyond the scoping concern, this code has two functional problems:
-
Regresses PR #104's timestamp fix.
write_data_polling()has notimestamp_offsetparameter, so when the polling fallback triggers, the timestamp is not updated before the write. This undoes the latency accuracy fix from #104. -
Missing condvar signal after polling write.
write_data_polling()writes data and updateswrite_posbut never callspthread_cond_signal(&data_ready). A reader blocked on the condvar path won't wake until its 500µs timed wait expires. PR #103 fixed this same class of issue.
Please scope this PR to issue #107 items 4-5 (buffer sizing and tests). The condvar/polling work (items 1-3) should be excluded — those issue items should be moved to a future PR when the container-IPC architecture is ready.
Minor: overlapping files
.cargo/audit.toml, Cargo.lock, and Cargo.toml changes overlap with PRs #103, #104, and #105. Whichever merges last will need to resolve conflicts. Not a blocker, just a heads-up.
|
Issue #107 updated to reflect this PR only addresses: Fix SHM automatic buffer sizing to use a fixed 64 KB buffer (or 2x message size for large messages) instead of sizing the ring buffer to fit all messages at once. A small buffer forces natural writer/reader pacing through backpressure, producing latencies representative of steady-state IPC performance. Update test_transport_config_buffer_size_logic to validate the new SHM-specific fixed-buffer behavior while preserving message-count-based sizing for TCP/UDS. Here's a summary of what was removed/restored in src/ipc/shared_memory_blocking.rs (net -270 lines): Removed:
Restored from main:
All validation passes: cargo fmt, cargo check, cargo test (42 passed), cargo clippy (0 warnings). Both functional problems are fully resolved by the removal. Here's why:
Both problems existed exclusively in the polling fallback functions and the broken-condvar detection logic that routed into them. Since all of that code is removed, neither issue can occur. The write and read paths now match exactly what's on main (from PR #104), with clean pthread_cond_wait and proper signaling in both directions. |
dustinblack
left a comment
There was a problem hiding this comment.
Thanks for removing the condvar/polling code. The buffer sizing fix looks good and is ready to go, but this branch needs a rebase on main before I can approve — it's based on e826b20, missing PRs #103 and #104.
After rebasing, please also clean up the duplication:
shared_memory_blocking.rstimestamp fix — already merged via #104. After rebase this should drop out automatically, but verify no duplicate changes remain..cargo/audit.toml— already on main via #103. Same.- README "Streaming Output Columns" and "Test Execution Order" sections — these are out of scope for a buffer-sizing PR. Please remove them; if they're needed, they belong in a separate PR with their own justification.
The MSRV pins in Cargo.toml (uuid <1.21, zmij, quote, syn, unicode-ident, tempfile <3.25) look like legitimate CI fixes and are fine to keep.
Previously, standalone SHM mode calculated buffer size to fit ALL
messages (e.g., 6.4MB for 50k messages). This caused the writer to
dump everything instantly while the reader slowly drained using
pthread_cond_timedwait with 500us polling timeouts, leading to huge
accumulated latencies (~489ms for 64B).
The fix uses a fixed 64KB buffer (or 2x message size if larger),
matching container behavior. This enables proper streaming where the
writer blocks when the buffer is full.
Before: 489ms mean latency for 64B/50k messages
After: 1.95ms mean latency for 64B/10k messages (blocking)
15.85ms mean latency for 64B/10k messages (async)
Also updates test_transport_config_buffer_size_logic to expect the
new fixed-buffer behavior for SHM while keeping the message-count
sizing for TCP/UDS.
Cherry-picked from container-to-container-ipc branch (3b49877).
AI-assisted-by: Claude claude-4.6-opus-high-thinking (Anthropic)
Made-with: Cursor
…entation
- Add 9 new tests covering SHM buffer sizing, backpressure, and
condvar timed-wait behavior:
- test_shm_large_message_buffer_sizing: verifies 2x msg size path
when messages exceed 32KB (async)
- test_shm_duration_mode_uses_fixed_buffer: verifies SHM gets 64KB
in duration mode, not 1GB TCP/UDS default (async)
- test_blocking_transport_config_buffer_size_logic: full buffer
sizing test for blocking mode (SHM, TCP, PMQ, duration)
- test_blocking_shm_duration_mode_uses_fixed_buffer: SHM 64KB in
blocking duration mode
- test_blocking_shm_large_message_buffer_sizing: 2x path (blocking)
- test_backpressure_with_small_buffer: exercises timed condvar wait
with 1KB buffer and 20 messages
- test_payload_integrity_under_backpressure: byte-level payload
verification through backpressure-induced blocking writes
- test_ring_buffer_wrap_around_under_backpressure: write_pos wraps
the circular buffer multiple times under backpressure
- test_shutdown_detected_during_blocked_write: server closes while
client is blocked waiting for buffer space
- Update README.md Buffer Size Configuration with per-mechanism
auto-sizing table and updated error prevention guidance
- Update CONFIG.md SHM defaults from 8192 to 64KB (auto) and add
automatic buffer sizing explanation
- All tests passing, clippy clean
AI-assisted-by: Claude claude-4.6-opus-high-thinking (Anthropic)
Made-with: Cursor
- Add test_user_buffer_size_overrides_shm_default (async + blocking): verifies user-provided --buffer-size overrides SHM's 64KB default - Add test_shm_buffer_sizing_at_32kb_boundary (async + blocking): tests exact transition where 2*(msg_size+64) crosses 64KB threshold - Add test_high_volume_condvar_stress: 100 messages through 512-byte buffer to stress pthread_cond_timedwait retry loop - Update create_transport_config_internal doc comment in benchmark.rs to describe per-mechanism buffer sizing (SHM, PMQ, TCP/UDS) - Update Adaptive Buffer Sizing doc in benchmark_blocking.rs to describe per-mechanism behavior instead of vague description - Fix README.md example output: SharedMemory buffer size 10240000 -> 65536 to reflect new fixed 64KB auto-sizing - All 340 tests passing, zero clippy warnings AI-assisted-by: Claude claude-4.6-opus-high-thinking Made-with: Cursor
…ility The CI MSRV job removes Cargo.lock and resolves fresh dependencies. Several transitive dependencies recently bumped their MSRV above 1.70: - uuid 1.21+ requires Rust 1.85 → pinned to <1.21 - tempfile 3.25+ pulls getrandom >=0.3,<0.5 which resolves to 0.4.x (edition 2024, unparseable by Rust 1.70's cargo) → pinned to <3.25 - zmij 1.0.20+ requires Rust 1.71 → pinned to =1.0.19 - quote 1.0.45+ requires Rust 1.71 → pinned to =1.0.44 - syn 2.0.115+ requires Rust 1.71 → pinned to =2.0.114 - unicode-ident 1.0.23+ requires Rust 1.71 → pinned to =1.0.22 Verified: MSRV builds and tests pass both with and without Cargo.lock in a Rust 1.70 container. Local clippy, fmt, and tests all clean. AI-assisted-by: Claude claude-4.6-opus-high-thinking Made-with: Cursor
No code changes. Forces new CI run to pick up dependency pins from commit cd28295 (uuid <1.21, tempfile <3.25, zmij =1.0.19, quote =1.0.44, syn =2.0.114, unicode-ident =1.0.22). AI-assisted-by: Claude claude-4.6-opus-high-thinking Made-with: Cursor
Remove container-IPC code that was out of scope for issue #107 (buffer sizing fix). This scopes the PR to items 4-5 only. Removed: - write_data_polling() and read_data_polling() fallback functions - pthread_cond_timedwait (reverted to pthread_cond_wait) - Broken-condvar detection (100-iteration/10ms heuristic) - Mutex-lock-failure fallbacks to polling paths - 30-second timeout counters (wait_count > 60000) - test_high_volume_condvar_stress test Restored from main (PR #104): - write_data_blocking() signature with timestamp_offset parameter so latency measurement excludes backpressure wait time - read_data_blocking() with clean pthread_cond_wait - Proper pthread_cond_signal in both write and read paths The two functional regressions cited in review are resolved: 1. Timestamp regression: write_data_polling() lacked timestamp_offset, but that function no longer exists. The only write path now refreshes the timestamp after the condvar wait. 2. Missing condvar signal: write_data_polling() never called pthread_cond_signal(&data_ready), but that function no longer exists. The only write path signals after every write. All tests passing (42/42). No clippy warnings. AI-assisted-by: Claude claude-4.6-opus-high-thinking Made-with: Cursor
Remove sections that belong in the streaming-timestamps PR (#105), not this buffer-sizing PR: - "Streaming Output Columns" table and timestamp_ns accuracy note - "Test Execution Order" section - Streaming JSON/CSV description rewording - Round-trip CLI example comment expansion README diff now only contains buffer-sizing documentation changes: auto-sizing table, error prevention updates, and example buffer size correction. AI-assisted-by: Claude claude-4.6-opus-high-thinking Made-with: Cursor
44aaa3f to
c64e906
Compare
|
PR #108 is updated with 7 clean commits rebased on main: a7690b9 — Fix: Use fixed 64KB buffer for standalone SHM All items addressed: No duplicate .cargo/audit.toml (dropped by rebase) |
dustinblack
left a comment
There was a problem hiding this comment.
Rebase looks clean — duplication from #103/#104 dropped out, out-of-scope README sections removed. The remaining diff is focused on buffer sizing with good test coverage.
The beta CI failure (tcp_one_way_blocking_smoke) is a flaky integration test unrelated to these changes — all other platforms and stable pass.
Approved.
📈 Changed lines coverage: 83.33% (10/12)🚨 Uncovered lines in this PR
📊 Code Coverage Summary
|
|
Question: Intentional difference in message overhead values? I noticed that
Is the Applies to:
|
Replace hardcoded 64 and 32 values with the MESSAGE_OVERHEAD constant in both benchmark.rs and benchmark_blocking.rs: - TCP/UDS msg-count sizing: was hardcoded 64, now MESSAGE_OVERHEAD - SHM logging/validation: was hardcoded 32, now MESSAGE_OVERHEAD - Add comment explaining what MESSAGE_OVERHEAD covers: 8 (id) + 8 (timestamp) + 8 (bincode vec length) + 1 (message type) + 4 (ring buffer length prefix) = 29 bytes, rounded to 64 Addresses review feedback about inconsistent overhead values. AI-assisted-by: Claude claude-4.6-opus-high-thinking Made-with: Cursor
|
It should always be 64 bytes. |
📈 Changed lines coverage: 88.89% (16/18)🚨 Uncovered lines in this PR
📊 Code Coverage Summary
|
sberg-rh
left a comment
There was a problem hiding this comment.
Looks good with latest updates! Approved
Description
Brief description of changes
Type of Change
Testing
Checklist
PR: Fix SHM Buffer Over-Sizing and Condvar Hangs
Summary
Shared memory (SHM) benchmarks produced misleading latency measurements
because the auto-sized ring buffer was large enough to hold all messages
at once. The writer dumped everything instantly while the reader slowly
drained the buffer, causing artificially huge accumulated latencies for
later messages. Additionally, the blocking SHM ring buffer used
pthread_cond_wait(infinite wait) for synchronization, which couldhang indefinitely when condition variable signals were missed — a known
failure mode in containerized environments with glibc ABI mismatches.
This PR replaces the all-messages-at-once buffer sizing with a fixed
64KB streaming buffer for SHM, and replaces infinite condvar waits
with timed waits plus a polling fallback for robustness.
Branch:
fix/buffer-size-overloadingBase:
mainFiles changed: 6 (802 insertions, 43 deletions)
Problems and Fixes
1. SHM buffer sized to fit all messages (batching instead of streaming)
Problem: When
--buffer-sizewas omitted, both async and blockingbenchmark runners auto-sized the SHM ring buffer to fit every message:
msg_count * (msg_size + 64). For a 10,000-message × 1KB benchmark,this created a ~10MB buffer. The writer filled it instantly without
ever blocking, while the reader drained messages one at a time. Later
messages sat in the buffer for seconds, accumulating artificial latency
that had nothing to do with IPC performance.
Before:
After:
Effect: SHM now uses a fixed 64KB ring buffer (or 2x message size
for messages larger than 32KB). The writer blocks when the buffer is
full, producing per-message latencies that reflect actual IPC
throughput rather than queue drain time.
msg_count * (msg_size + 64)msg_count * (msg_size + 64)Applies to:
src/benchmark.rs(async) andsrc/benchmark_blocking.rs(blocking) — identical logic in both.2. SHM condvar infinite wait causing hangs
Problem:
write_data_blocking()andread_data_blocking()usedpthread_cond_wait()with no timeout. If the condition variable signalwas missed (common in cross-process scenarios, especially in containers
with glibc ABI mismatches), the thread would hang indefinitely.
Before:
After:
The same pattern was applied to
read_data_blocking().Changes:
pthread_cond_wait→pthread_cond_timedwaitwith 500µs timeout3. No fallback when pthread primitives fail
Problem: If
pthread_mutex_lockfailed orpthread_cond_timedwaitmalfunctioned (both possible in containerized environments with
mismatched glibc versions), there was no alternative code path. The
operation would either crash or hang.
Fix: Added
write_data_polling()andread_data_polling()methodsthat use
thread::sleep(Duration::from_micros(100))polling with a30-second timeout. These are activated when:
pthread_mutex_lockreturns non-zeroApplies to:
src/ipc/shared_memory_blocking.rs— bothwrite_data_polling()andread_data_polling().4. SHM buffer warning log was misleading
Problem: When the SHM buffer was smaller than total data volume,
the benchmark logged a
warn!saying "This may cause backpressure,which is a valid test scenario." With the new fixed 64KB buffer,
this warning fired on virtually every SHM run and was no longer
indicating an unusual condition — it was the intended design.
Before:
After:
Applies to: Both
src/benchmark.rsandsrc/benchmark_blocking.rs.Tests Added
Buffer sizing tests (5 tests)
test_transport_config_buffer_size_logic(updated)benchmark.rstest_shm_large_message_buffer_sizingbenchmark.rstest_shm_duration_mode_uses_fixed_bufferbenchmark.rstest_blocking_transport_config_buffer_size_logicbenchmark_blocking.rstest_blocking_shm_duration_mode_uses_fixed_bufferbenchmark_blocking.rstest_blocking_shm_large_message_buffer_sizingbenchmark_blocking.rsSHM backpressure and robustness tests (4 tests)
test_backpressure_with_small_buffershared_memory_blocking.rspthread_cond_timedwaitpathtest_payload_integrity_under_backpressureshared_memory_blocking.rstest_ring_buffer_wrap_around_under_backpressureshared_memory_blocking.rstest_shutdown_detected_during_blocked_writeshared_memory_blocking.rsDocumentation Updated
CONFIG.md
--buffer-sizedefault changed from8192to*auto*buffer_sizedefault changed from8192to64 KB (auto)"leave at automatic"
README.md
auto-sizing table
messages
Validation
Unit and Integration Tests
Before/After: SHM Buffer Sizing (Actual Test Results)
Test command (identical on both branches):
./target/release/ipc-benchmark -m shm --blocking -i 1000 -s 1024 \ --one-way -w 0 --streaming-output-csv output.csvBefore fix (
mainbranch):The writer dumps all 1000 messages into the 1MB buffer instantly.
The reader drains them one at a time. By message 250, latency has
climbed to 2.7ms and stays there — this is queue-drain time, not
IPC latency.
After fix (
fix/buffer-size-overloadingbranch):The writer blocks when the 64KB buffer fills (~60 messages), waits
for the reader to drain space, then continues. Every message's
latency reflects actual IPC throughput.
Improvement:
main)Throughput is nearly identical because the total data transfer rate
is the same — the fix changes when messages are written, not how
fast the IPC channel moves data.
Before/After: Condvar Wait Behavior
Before fix (
mainbranch):write_data_blocking()andread_data_blocking()usedpthread_cond_wait()with no timeout.If a condition variable signal was missed (common in cross-process
scenarios, especially in containers with glibc ABI mismatches), the
thread would hang indefinitely — no timeout, no error, no recovery.
After fix:
pthread_cond_timedwaitwith 500µs timeout means amissed signal costs at most 500µs of latency, not an infinite hang.
If the condvar is completely broken (100 no-ops in <10ms), the code
falls back to polling. If nothing works, a 30-second timeout produces
a clear error message:
Files Changed
src/ipc/shared_memory_blocking.rspthread_cond_waitwithpthread_cond_timedwait(500µs) in bothwrite_data_blockingandread_data_blocking; add broken-condvar detection; addwrite_data_polling()andread_data_polling()fallbacks; add mutex lock failure handling; add 30-second overall timeouts; 4 new backpressure/robustness testssrc/benchmark.rswarn!todebug!; update existing buffer test; add 2 new SHM buffer sizing testssrc/benchmark_blocking.rsDurationimport; add 3 new blocking buffer sizing testsREADME.mdCONFIG.md--buffer-sizedefault to auto; update SHM buffer_size default and optimal settings.cargo/audit.tomlRisk Assessment
--buffer-sizeis omitted. User-provided values are alwaysrespected. TCP, UDS, and PMQ behavior is completely unchanged.
pthread condvars — just with a timeout instead of infinite wait.
The polling fallback only activates on detected failures.
Users who were explicitly passing
--buffer-sizesee no difference.backpressure behavior introduced by this branch.