Skip to content

Commit 1cba5c9

Browse files
authored
Merge pull request #28 from antoninkriz/deadlock-fix
Fix marker worker result deadlock
2 parents adb1386 + ad34b80 commit 1cba5c9

2 files changed

Lines changed: 214 additions & 39 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
55

66
## [Unreleased]
77

8+
### Fixed
9+
10+
- Marker/window resolution waits now drain the bounded speculative-result
11+
channel. This prevents all shared workers from blocking while publishing
12+
decode results when the coordinator needs queued resolution work to finish.
13+
- Parallel-encoder sync flushes can no longer force an otherwise suitable
14+
gzip or zlib stream off the marker/window path. Small discontinuities between
15+
speculative chunks are exact-decoded with authenticated history and retain
16+
the existing safe fallback for invalid or larger gaps.
17+
818
## [0.2.0] - 2026-08-04
919

1020
### Upgrade notes

crates/rapidgzip-core/src/backend.rs

Lines changed: 204 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use libz_rs_sys as z;
2929
use std::collections::BTreeMap;
3030
use std::io::{Read, Write};
3131
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
32-
use std::sync::mpsc::{self, RecvTimeoutError, TrySendError};
32+
use std::sync::mpsc::{self, RecvTimeoutError, TryRecvError, TrySendError};
3333
use std::sync::{Arc, Condvar, Mutex};
3434
use std::thread;
3535
use std::time::{Duration, Instant};
@@ -2838,6 +2838,77 @@ struct NativeResult {
28382838
result: Result<crate::parallel::deflate::Chunk, NativeError>,
28392839
}
28402840

2841+
/// Coordinator-owned native results that have arrived ahead of output order.
2842+
///
2843+
/// Decode and marker-resolution work share one worker pool but publish through
2844+
/// separate bounded channels. A coordinator waiting for resolution must keep
2845+
/// draining this inbox so every worker cannot become blocked publishing native
2846+
/// work. The scheduler bounds the number of queued and in-flight decode tasks;
2847+
/// moving their completed results here therefore preserves the existing memory
2848+
/// bound without making either result channel unbounded.
2849+
struct NativeResultInbox {
2850+
receiver: mpsc::Receiver<NativeResult>,
2851+
pending: BTreeMap<usize, Result<crate::parallel::deflate::Chunk, NativeError>>,
2852+
}
2853+
2854+
impl NativeResultInbox {
2855+
fn new(receiver: mpsc::Receiver<NativeResult>) -> Self {
2856+
Self {
2857+
receiver,
2858+
pending: BTreeMap::new(),
2859+
}
2860+
}
2861+
2862+
fn retain_from(&mut self, next_index: usize) {
2863+
self.pending.retain(|index, _| *index >= next_index);
2864+
}
2865+
2866+
#[inline]
2867+
fn offer(&mut self, result: NativeResult, next_index: usize) {
2868+
if result.index >= next_index {
2869+
self.pending.insert(result.index, result.result);
2870+
}
2871+
}
2872+
2873+
/// Moves every currently available native result out of the bounded
2874+
/// worker channel, discarding results made stale by a member transition.
2875+
fn drain_available(&mut self, next_index: usize) {
2876+
while let Ok(result) = self.receiver.try_recv() {
2877+
self.offer(result, next_index);
2878+
}
2879+
}
2880+
2881+
/// Receives one native result in task-index order.
2882+
#[inline]
2883+
fn receive(
2884+
&mut self,
2885+
next_index: usize,
2886+
cancelled: &AtomicBool,
2887+
) -> Result<Result<crate::parallel::deflate::Chunk, NativeError>, DecodeError> {
2888+
loop {
2889+
if let Some(result) = self.pending.remove(&next_index) {
2890+
return Ok(result);
2891+
}
2892+
match self.receiver.recv_timeout(Duration::from_millis(10)) {
2893+
Ok(result) => self.offer(result, next_index),
2894+
Err(RecvTimeoutError::Timeout) => {
2895+
if cancelled.load(Ordering::Relaxed) {
2896+
return Err(DecodeError::Cancelled);
2897+
}
2898+
}
2899+
Err(RecvTimeoutError::Disconnected) => {
2900+
return Err(DecodeError::WorkerPanicked);
2901+
}
2902+
}
2903+
}
2904+
}
2905+
2906+
#[cfg(test)]
2907+
fn pending_indices(&self) -> Vec<usize> {
2908+
self.pending.keys().copied().collect()
2909+
}
2910+
}
2911+
28412912
struct ResolveTask {
28422913
sequence: usize,
28432914
predecessor: Window,
@@ -3370,21 +3441,44 @@ fn emit_resolved_parts<O: Output>(
33703441
Ok(())
33713442
}
33723443

3444+
#[allow(clippy::too_many_arguments)]
33733445
fn wait_for_resolved(
3374-
receiver: &mpsc::Receiver<ResolveResult>,
3375-
pending: &mut BTreeMap<usize, Result<ResolvedParts, crate::parallel::MarkerError>>,
3446+
resolve_receiver: &mpsc::Receiver<ResolveResult>,
3447+
resolve_pending: &mut BTreeMap<usize, Result<ResolvedParts, crate::parallel::MarkerError>>,
3448+
native_results: &mut NativeResultInbox,
3449+
next_native_index: usize,
33763450
next_sequence: usize,
33773451
cancelled: &AtomicBool,
33783452
bit_offset: u64,
33793453
) -> Result<ResolvedParts, DecodeError> {
33803454
let result = loop {
3381-
if let Some(result) = pending.remove(&next_sequence) {
3455+
if let Some(result) = resolve_pending.remove(&next_sequence) {
33823456
break result;
33833457
}
3384-
match receiver.recv_timeout(Duration::from_millis(10)) {
3458+
3459+
// Preserve the original immediate-result fast path. Native draining
3460+
// is needed only when the required resolution is not already waiting.
3461+
match resolve_receiver.try_recv() {
3462+
Ok(result) if result.sequence < next_sequence => continue,
3463+
Ok(result) if result.sequence == next_sequence => break result.result,
3464+
Ok(result) => {
3465+
resolve_pending.insert(result.sequence, result.result);
3466+
continue;
3467+
}
3468+
Err(TryRecvError::Disconnected) => return Err(DecodeError::WorkerPanicked),
3469+
Err(TryRecvError::Empty) => {}
3470+
}
3471+
3472+
// A resolution worker cannot run if every shared worker is blocked on
3473+
// the bounded native-result channel. Drain that channel before each
3474+
// short resolution wait to break the circular dependency. The short
3475+
// interval also bounds the race in which native results arrive after
3476+
// the drain but before this receive begins.
3477+
native_results.drain_available(next_native_index);
3478+
match resolve_receiver.recv_timeout(Duration::from_millis(1)) {
33853479
Ok(result) if result.sequence < next_sequence => {}
33863480
Ok(result) => {
3387-
pending.insert(result.sequence, result.result);
3481+
resolve_pending.insert(result.sequence, result.result);
33883482
}
33893483
Err(RecvTimeoutError::Timeout) => {
33903484
if cancelled.load(Ordering::Relaxed) {
@@ -3450,8 +3544,10 @@ fn enqueue_native_resolution(
34503544

34513545
#[allow(clippy::too_many_arguments)]
34523546
fn drain_native_resolutions<O: Output>(
3453-
receiver: &mpsc::Receiver<ResolveResult>,
3454-
pending: &mut BTreeMap<usize, Result<ResolvedParts, crate::parallel::MarkerError>>,
3547+
resolve_receiver: &mpsc::Receiver<ResolveResult>,
3548+
resolve_pending: &mut BTreeMap<usize, Result<ResolvedParts, crate::parallel::MarkerError>>,
3549+
native_results: &mut NativeResultInbox,
3550+
next_native_index: usize,
34553551
next_sequence: &mut usize,
34563552
outstanding: &mut usize,
34573553
cancelled: &AtomicBool,
@@ -3462,7 +3558,15 @@ fn drain_native_resolutions<O: Output>(
34623558
total_output: &mut u64,
34633559
) -> Result<(), DecodeError> {
34643560
while *outstanding != 0 {
3465-
let parts = wait_for_resolved(receiver, pending, *next_sequence, cancelled, bit_offset)?;
3561+
let parts = wait_for_resolved(
3562+
resolve_receiver,
3563+
resolve_pending,
3564+
native_results,
3565+
next_native_index,
3566+
*next_sequence,
3567+
cancelled,
3568+
bit_offset,
3569+
)?;
34663570
emit_resolved_parts(parts, config, output, accounting, total_output)?;
34673571
*next_sequence += 1;
34683572
*outstanding -= 1;
@@ -3944,12 +4048,10 @@ where
39444048
Arc::clone(runtime),
39454049
));
39464050
let worker_pool_count = adaptive_workers.worker_pool_limit().min(worker_count);
3947-
// Result channels need spare slots beyond the active ranks because the
3948-
// same pool executes marker resolution. If every worker blocks while
3949-
// publishing speculative decode, no rank remains to resolve an exact
3950-
// member bridge. The configured window carries the usual two-slot slack;
3951-
// the scheduling horizon below still follows the adaptive active limit, so
3952-
// this capacity does not admit extra speculative tasks.
4051+
// Decode and marker-resolution tasks share this worker pool. Both result
4052+
// channels use the bounded scheduling horizon; resolution waits drain the
4053+
// native inbox so its backpressure cannot keep every worker from servicing
4054+
// the higher-priority resolution queue.
39534055
let pipeline_capacity = config
39544056
.in_flight_chunks
39554057
.max(worker_pool_count)
@@ -3967,7 +4069,7 @@ where
39674069
let mut current_bit = first_deflate_bit;
39684070
let mut next_to_schedule = initial_task_window;
39694071
let mut next_to_emit = 0_usize;
3970-
let mut pending = BTreeMap::new();
4072+
let mut native_results = NativeResultInbox::new(receiver);
39714073
let mut prepared_total_output = 0_u64;
39724074
let mut next_resolve_sequence = 0_usize;
39734075
let mut next_resolve_to_emit = 0_usize;
@@ -4033,6 +4135,8 @@ where
40334135
drain_native_resolutions(
40344136
&resolve_receiver,
40354137
&mut resolve_pending,
4138+
&mut native_results,
4139+
next_to_emit,
40364140
&mut next_resolve_to_emit,
40374141
&mut outstanding_resolves,
40384142
cancelled,
@@ -4135,7 +4239,7 @@ where
41354239
continue 'decode;
41364240
}
41374241

4138-
pending.retain(|index, _| *index >= target_index);
4242+
native_results.retain_from(target_index);
41394243
next_to_emit = target_index;
41404244
if next_to_schedule < target_index {
41414245
next_to_schedule = target_index;
@@ -4193,6 +4297,8 @@ where
41934297
let parts = wait_for_resolved(
41944298
&resolve_receiver,
41954299
&mut resolve_pending,
4300+
&mut native_results,
4301+
next_to_emit,
41964302
next_resolve_to_emit,
41974303
cancelled,
41984304
current_bit,
@@ -4232,25 +4338,7 @@ where
42324338
if cancelled.load(Ordering::Relaxed) {
42334339
return Err(DecodeError::Cancelled);
42344340
}
4235-
let result = loop {
4236-
if let Some(result) = pending.remove(&next_to_emit) {
4237-
break result;
4238-
}
4239-
match receiver.recv_timeout(Duration::from_millis(10)) {
4240-
Ok(result) if result.index < next_to_emit => {}
4241-
Ok(result) => {
4242-
pending.insert(result.index, result.result);
4243-
}
4244-
Err(RecvTimeoutError::Timeout) => {
4245-
if cancelled.load(Ordering::Relaxed) {
4246-
return Err(DecodeError::Cancelled);
4247-
}
4248-
}
4249-
Err(RecvTimeoutError::Disconnected) => {
4250-
return Err(DecodeError::WorkerPanicked);
4251-
}
4252-
}
4253-
};
4341+
let result = native_results.receive(next_to_emit, cancelled)?;
42544342

42554343
let mut gap_bridge = None;
42564344
let chunk = match result {
@@ -4277,6 +4365,8 @@ where
42774365
drain_native_resolutions(
42784366
&resolve_receiver,
42794367
&mut resolve_pending,
4368+
&mut native_results,
4369+
next_to_emit,
42804370
&mut next_resolve_to_emit,
42814371
&mut outstanding_resolves,
42824372
cancelled,
@@ -4325,6 +4415,8 @@ where
43254415
let parts = wait_for_resolved(
43264416
&resolve_receiver,
43274417
&mut resolve_pending,
4418+
&mut native_results,
4419+
next_to_emit,
43284420
next_resolve_to_emit,
43294421
cancelled,
43304422
current_bit,
@@ -4353,6 +4445,8 @@ where
43534445
let parts = wait_for_resolved(
43544446
&resolve_receiver,
43554447
&mut resolve_pending,
4448+
&mut native_results,
4449+
next_to_emit,
43564450
next_resolve_to_emit,
43574451
cancelled,
43584452
current_bit,
@@ -4382,6 +4476,8 @@ where
43824476
drain_native_resolutions(
43834477
&resolve_receiver,
43844478
&mut resolve_pending,
4479+
&mut native_results,
4480+
next_to_emit,
43854481
&mut next_resolve_to_emit,
43864482
&mut outstanding_resolves,
43874483
cancelled,
@@ -4902,10 +4998,16 @@ where
49024998
mod tests {
49034999
use super::{
49045000
INDEPENDENT_MEMBER_TASK_MAX_CANDIDATES, InputCursor, MAX_EXACT_GAP_BITS, MemberAccounting,
4905-
MemberHeader, SourceCursor, Window, batch_independent_headers, bridge_exact_gap,
4906-
find_gzip_magic, find_gzip_magic_scalar, independent_member_task_candidate_limit,
4907-
inflate_tail, validate_footer,
5001+
MemberHeader, NativeError, NativeResult, NativeResultInbox, ResolveResult, SourceCursor,
5002+
Window, batch_independent_headers, bridge_exact_gap, find_gzip_magic,
5003+
find_gzip_magic_scalar, independent_member_task_candidate_limit, inflate_tail,
5004+
send_native_result, send_resolve_result, validate_footer, wait_for_resolved,
49085005
};
5006+
use std::collections::BTreeMap;
5007+
use std::sync::atomic::AtomicBool;
5008+
use std::sync::{Arc, mpsc};
5009+
use std::thread;
5010+
use std::time::Duration;
49095011

49105012
fn header(start: u64) -> MemberHeader {
49115013
MemberHeader {
@@ -5119,4 +5221,67 @@ mod tests {
51195221
assert_eq!(validate_footer(&mut cursor, 6, 0, &accounting).unwrap(), 0);
51205222
assert_eq!(cursor.position(), 8);
51215223
}
5224+
5225+
#[test]
5226+
fn resolution_wait_drains_native_results_without_losing_order() {
5227+
let (finished_sender, finished_receiver) = mpsc::channel();
5228+
5229+
thread::spawn(move || {
5230+
let stopped = Arc::new(AtomicBool::new(false));
5231+
let (native_sender, native_receiver) = mpsc::sync_channel(2);
5232+
let (resolve_sender, resolve_receiver) = mpsc::sync_channel(1);
5233+
for index in [0, 1] {
5234+
native_sender
5235+
.send(NativeResult {
5236+
index,
5237+
result: Err(NativeError::UnexpectedEof),
5238+
})
5239+
.unwrap();
5240+
}
5241+
5242+
let worker_stopped = Arc::clone(&stopped);
5243+
let worker = thread::spawn(move || {
5244+
send_native_result(
5245+
&native_sender,
5246+
&worker_stopped,
5247+
NativeResult {
5248+
index: 2,
5249+
result: Err(NativeError::UnexpectedEof),
5250+
},
5251+
);
5252+
send_resolve_result(
5253+
&resolve_sender,
5254+
&worker_stopped,
5255+
ResolveResult {
5256+
sequence: 0,
5257+
result: Ok((vec![1], vec![2], vec![3])),
5258+
},
5259+
);
5260+
});
5261+
5262+
let mut native_results = NativeResultInbox::new(native_receiver);
5263+
let mut resolve_pending = BTreeMap::new();
5264+
let resolved = wait_for_resolved(
5265+
&resolve_receiver,
5266+
&mut resolve_pending,
5267+
&mut native_results,
5268+
1,
5269+
0,
5270+
&stopped,
5271+
0,
5272+
)
5273+
.unwrap();
5274+
worker.join().unwrap();
5275+
native_results.drain_available(1);
5276+
finished_sender
5277+
.send((resolved, native_results.pending_indices()))
5278+
.unwrap();
5279+
});
5280+
5281+
let (resolved, native_indices) = finished_receiver
5282+
.recv_timeout(Duration::from_secs(5))
5283+
.expect("resolution wait deadlocked behind the full native-result channel");
5284+
assert_eq!(resolved, (vec![1], vec![2], vec![3]));
5285+
assert_eq!(native_indices, [1, 2]);
5286+
}
51225287
}

0 commit comments

Comments
 (0)