@@ -193,6 +193,8 @@ pub const Collection = struct {
193193 index_thread : ? std.Thread ,
194194 index_thread2 : ? std.Thread ,
195195 index_stop : std .atomic .Value (bool ),
196+ /// Futex signal — index workers sleep on this when queues are empty.
197+ index_wake : std .atomic .Value (u32 ),
196198 queue_toggle : std .atomic .Value (u32 ),
197199 indexing_count : std .atomic .Value (u32 ),
198200
@@ -248,6 +250,7 @@ pub const Collection = struct {
248250 col .index_queue = IndexQueue .init (alloc );
249251 col .index_queue2 = IndexQueue .init (alloc );
250252 col .index_stop = std .atomic .Value (bool ).init (false );
253+ col .index_wake = std .atomic .Value (u32 ).init (0 );
251254 col .queue_toggle = std .atomic .Value (u32 ).init (0 );
252255 col .indexing_count = std .atomic .Value (u32 ).init (0 );
253256 col .vectors = null ;
@@ -271,6 +274,8 @@ pub const Collection = struct {
271274 pub fn close (self : * Collection ) void {
272275 // Signal background indexers to stop, then join both threads.
273276 self .index_stop .store (true , .release );
277+ // Wake sleeping index workers so they observe the stop flag.
278+ std .Thread .Futex .wake (& self .index_wake , std .math .maxInt (u32 ));
274279 if (self .index_thread ) | t | t .join ();
275280 if (self .index_thread2 ) | t | t .join ();
276281 // Drain any leftover entries from both queues (free owned slices).
@@ -322,9 +327,14 @@ pub const Collection = struct {
322327 /// Block until the background indexers have drained all pending items
323328 /// and finished processing the current batch.
324329 pub fn flushIndex (self : * Collection ) void {
325- var waited : u32 = 0 ;
326- while ((self .index_queue .len () > 0 or self .index_queue2 .len () > 0 or self .indexing_count .load (.acquire ) > 0 ) and waited < 300_000 ) : (waited += 1 ) {
327- std .Thread .sleep (100_000 ); // 100µs
330+ var sleep_ns : u64 = 100_000 ; // start at 100µs
331+ var total_ns : u64 = 0 ;
332+ const max_ns : u64 = 30_000_000_000 ; // 30s total cap
333+ while ((self .index_queue .len () > 0 or self .index_queue2 .len () > 0 or self .indexing_count .load (.acquire ) > 0 ) and total_ns < max_ns ) {
334+ std .Thread .sleep (sleep_ns );
335+ total_ns += sleep_ns ;
336+ // Exponential backoff: 100µs → 200µs → ... → 10ms cap.
337+ sleep_ns = @min (sleep_ns * 2 , 10_000_000 );
328338 }
329339 }
330340
@@ -425,6 +435,9 @@ pub const Collection = struct {
425435 }
426436 std .Thread .yield () catch {};
427437 }
438+ // Wake sleeping index worker to process the new entry.
439+ _ = self .index_wake .fetchAdd (1 , .release );
440+ std .Thread .Futex .wake (& self .index_wake , 1 );
428441 }
429442 }
430443 }
@@ -1434,7 +1447,10 @@ fn indexWorkerQ(col: *Collection, queue: *IndexQueue) void {
14341447 }
14351448 if (n == 0 ) {
14361449 _ = col .indexing_count .fetchSub (1 , .release );
1437- std .Thread .yield () catch {};
1450+ // Sleep on futex instead of spinning — woken by index push path.
1451+ const cur = col .index_wake .load (.acquire );
1452+ if (! col .index_stop .load (.acquire ))
1453+ std .Thread .Futex .timedWait (& col .index_wake , cur , 50_000_000 ) catch {};
14381454 continue ;
14391455 }
14401456 // Batch trigram indexing (single lock acquisition for all docs).
0 commit comments