@@ -93,6 +93,7 @@ private readonly (int indexSpacing, int batchSize, int indexMask)[] AdaptiveTuni
9393 private readonly ILogger < StreamDB > ? _logger ;
9494 private readonly Task _bgFlushRunner ;
9595 private readonly ManualResetEventSlim _flushSignal = new ( false ) ;
96+ private readonly ManualResetEventSlim _flushIdle = new ( true ) ;
9697 private readonly Lock _maintenanceLock = new ( ) ; // Ensures only one maintenance operation at a time
9798 private readonly DeferrableRwLock _indexWriteLock = new ( ) ;
9899
@@ -127,11 +128,12 @@ private readonly (int indexSpacing, int batchSize, int indexMask)[] AdaptiveTuni
127128
128129 #endregion
129130
130- public StreamDB ( string ? baseDir = null , TimeSpan ? retentionPeriod = null , TimeSpan ? checkpointInterval = null , long jitterWindow = 0 , ILogger < StreamDB > ? logger = null )
131+ public StreamDB ( string ? baseDir = null , TimeSpan ? retentionPeriod = null , TimeSpan ? checkpointInterval = null , long jitterWindow = 0 , ILogger < StreamDB > ? logger = null , int initialAdaptiveIdx = 6 )
131132 {
132133 _logger = logger ;
133134 _baseDir = baseDir ?? "streams" ;
134135 _jitterWindow = jitterWindow ;
136+ _adaptiveIdx = Math . Clamp ( initialAdaptiveIdx , 0 , AdaptiveTuning . Length - 1 ) ;
135137 Directory . CreateDirectory ( _baseDir ) ;
136138
137139 _retentionPeriod = retentionPeriod ?? TimeSpan . FromDays ( 60 ) ;
@@ -258,6 +260,7 @@ private void FlushWorker()
258260 // Process all pending batches until queue is empty
259261 while ( ! _pendingIndexInserts . IsEmpty && ! _disposed )
260262 {
263+ _flushIdle . Reset ( ) ;
261264 // Check tuning on every batch for responsiveness (before acquiring lock)
262265 AdaptivelyTuneParameters ( ) ;
263266
@@ -346,6 +349,7 @@ private void FlushWorker()
346349 _indexWriteLock . ExitReadLock ( actualLockAcquired ) ;
347350 }
348351 }
352+ _flushIdle . Set ( ) ;
349353 }
350354 }
351355
@@ -462,9 +466,9 @@ public void Append(long primaryIndex, int secondaryIndex, ushort version, ReadOn
462466 int currentCount = Volatile . Read ( ref _pendingIndexCount ) ;
463467 if ( currentCount < QueueCapacity )
464468 {
469+ _flushIdle . Reset ( ) ;
465470 Interlocked . Increment ( ref _pendingIndexCount ) ;
466471 _pendingIndexInserts . Enqueue ( ( secondaryIndex , primaryIndex , address ) ) ;
467- // in aggressive scenarios as index frequency goes down we want to make sure the worker is signaled to process the larger batches in a timely manner
468472 _flushSignal . Set ( ) ;
469473 }
470474 else
@@ -482,16 +486,13 @@ public void Append(long primaryIndex, int secondaryIndex, ushort version, ReadOn
482486 /// </summary>
483487 public void WaitForPendingWrites ( )
484488 {
485- if ( _pendingIndexInserts . IsEmpty )
489+ if ( _pendingIndexInserts . IsEmpty && _flushIdle . IsSet )
486490 return ;
487491
488492 _flushSignal . Set ( ) ;
489493
490- // Wait for worker to process all entries
491- while ( ! _pendingIndexInserts . IsEmpty )
492- {
493- Thread . Yield ( ) ;
494- }
494+ // Wait for worker to drain the queue AND finish committing to SQLite
495+ _flushIdle . Wait ( ) ;
495496 }
496497
497498 /// <summary>
@@ -624,13 +625,27 @@ public Dictionary<int, List<StreamEntry>> ReadRange(IEnumerable<int> secondaryIn
624625 string . Join ( ", " , secondaryIndexes ) , startPrimaryIndex , endPrimaryIndex , limit ) ;
625626
626627 // Group secondary indexes by shard so we can scan each shard at most once.
628+ // Use forward lookup to skip devices whose data is entirely outside the query range.
627629 Dictionary < int , ( HashSet < int > Indexes , long MinAddress ) > shardGroups = new Dictionary < int , ( HashSet < int > Indexes , long MinAddress ) > ( ) ;
628630
629631 foreach ( int idx in secondaryIndexes )
630632 {
631- int shardIndex = idx & ShardMask ;
632633 long addr = LookupNearestAddress ( idx , startPrimaryIndex ) ;
633634
635+ if ( addr == 0 )
636+ {
637+ // No backward hit: use forward lookup to check if device has any indexed data.
638+ var forward = LookupFirstAddressAtOrAfter ( idx , startPrimaryIndex ) ;
639+ if ( ! forward . HasValue )
640+ {
641+ _logger ? . LogDebug ( "ReadRange (multi-index): skipping secondaryIndex={SecondaryIndex} — no indexed data" , idx ) ;
642+ continue ;
643+ }
644+ // Forward hit exists: device has data. Unindexed entries may exist before the
645+ // first index entry. Scan from shard beginning (addr stays 0).
646+ }
647+
648+ int shardIndex = idx & ShardMask ;
634649 if ( shardGroups . TryGetValue ( shardIndex , out ( HashSet < int > Indexes , long MinAddress ) group ) )
635650 {
636651 group . Indexes . Add ( idx ) ;
@@ -727,27 +742,53 @@ public Dictionary<int, List<StreamEntry>> ReadRange(long startPrimaryIndex, long
727742 _logger ? . LogDebug ( "GetEarliestPrimaryIndex: secondaryIndexes=[{Indexes}], from={FromPrimaryIndex}" ,
728743 string . Join ( ", " , secondaryIndexes ) , fromPrimaryIndex ) ;
729744
745+ long ? globalMin = null ;
730746 Dictionary < int , ( HashSet < int > Indexes , long MinAddress ) > shardGroups = new Dictionary < int , ( HashSet < int > Indexes , long MinAddress ) > ( ) ;
731747
732748 foreach ( int idx in secondaryIndexes )
733749 {
734750 int shardIndex = idx & ShardMask ;
735751 long addr = LookupNearestAddress ( idx , fromPrimaryIndex ) ;
736752
737- if ( shardGroups . TryGetValue ( shardIndex , out ( HashSet < int > Indexes , long MinAddress ) group ) )
753+ if ( addr > 0 )
738754 {
739- group . Indexes . Add ( idx ) ;
740- if ( addr < group . MinAddress )
741- shardGroups [ shardIndex ] = ( group . Indexes , addr ) ;
755+ // Backward hit: scan from this address.
756+ if ( shardGroups . TryGetValue ( shardIndex , out ( HashSet < int > Indexes , long MinAddress ) group ) )
757+ {
758+ group . Indexes . Add ( idx ) ;
759+ if ( addr < group . MinAddress )
760+ shardGroups [ shardIndex ] = ( group . Indexes , addr ) ;
761+ }
762+ else
763+ {
764+ shardGroups [ shardIndex ] = ( new HashSet < int > { idx } , addr ) ;
765+ }
742766 }
743767 else
744768 {
745- shardGroups [ shardIndex ] = ( new HashSet < int > { idx } , addr ) ;
769+ // No backward hit: use forward lookup to check if device has any indexed data.
770+ var forward = LookupFirstAddressAtOrAfter ( idx , fromPrimaryIndex ) ;
771+ if ( forward . HasValue )
772+ {
773+ // Forward hit: device has data but first index entry is after fromPrimaryIndex.
774+ // Unindexed entries may exist before it — scan from shard beginning.
775+ if ( shardGroups . TryGetValue ( shardIndex , out ( HashSet < int > Indexes , long MinAddress ) group ) )
776+ {
777+ group . Indexes . Add ( idx ) ;
778+ shardGroups [ shardIndex ] = ( group . Indexes , Math . Min ( group . MinAddress , 0 ) ) ;
779+ }
780+ else
781+ {
782+ shardGroups [ shardIndex ] = ( new HashSet < int > { idx } , 0 ) ;
783+ }
784+
785+ if ( ! globalMin . HasValue || forward . Value . PrimaryIndex < globalMin . Value )
786+ globalMin = forward . Value . PrimaryIndex ;
787+ }
788+ // else: no indexed data at all — skip this device
746789 }
747790 }
748791
749- long ? globalMin = null ;
750-
751792 _logger ? . LogDebug ( "GetEarliestPrimaryIndex: grouped into {ShardCount} shards" , shardGroups . Count ) ;
752793
753794 foreach ( ( int shardIndex , ( HashSet < int > ? indexes , long minAddress ) ) in shardGroups )
@@ -837,6 +878,37 @@ private long LookupNearestAddress(int secondaryIndex, long startPrimaryIndex)
837878 return addr ;
838879 }
839880
881+ /// <summary>
882+ /// Find the first indexed entry at or after <paramref name="primaryIndex"/> for this secondary index.
883+ /// Returns the primary index and FasterLog address, or null if no such entry exists.
884+ /// Uses the B-tree primary key index for O(log N) lookup.
885+ /// </summary>
886+ private ( long PrimaryIndex , long Address ) ? LookupFirstAddressAtOrAfter ( int secondaryIndex , long primaryIndex )
887+ {
888+ using PooledConnection pooled = GetConnection ( ) ;
889+ using SqliteCommand cmd = pooled . Connection . CreateCommand ( ) ;
890+ cmd . CommandText = "SELECT primary_index, log_address FROM stream_index WHERE secondary_index = $sidx AND primary_index >= $pi ORDER BY primary_index ASC LIMIT 1" ;
891+ SqliteParameter pSidx = cmd . Parameters . Add ( "$sidx" , SqliteType . Integer ) ;
892+ SqliteParameter pPi = cmd . Parameters . Add ( "$pi" , SqliteType . Integer ) ;
893+ cmd . Prepare ( ) ;
894+
895+ pSidx . Value = secondaryIndex ;
896+ pPi . Value = primaryIndex ;
897+ using SqliteDataReader reader = cmd . ExecuteReader ( ) ;
898+ if ( reader . Read ( ) )
899+ {
900+ long pi = reader . GetInt64 ( 0 ) ;
901+ long addr = reader . GetInt64 ( 1 ) ;
902+ _logger ? . LogDebug ( "LookupFirstAddressAtOrAfter: secondaryIndex={SecondaryIndex}, from={PrimaryIndex} -> pi={Pi}, address={Address}" ,
903+ secondaryIndex , primaryIndex , pi , addr ) ;
904+ return ( pi , addr ) ;
905+ }
906+
907+ _logger ? . LogDebug ( "LookupFirstAddressAtOrAfter: secondaryIndex={SecondaryIndex}, from={PrimaryIndex} -> null" ,
908+ secondaryIndex , primaryIndex ) ;
909+ return null ;
910+ }
911+
840912 /// <summary>
841913 /// Returns all distinct secondary indexes that have at least one indexed entry in the stream.
842914 /// </summary>
0 commit comments