@@ -140,6 +140,7 @@ impl State {
140140 tracing:: debug!( ?req, "ReadRequest dropped before registration" ) ;
141141 return ;
142142 }
143+ self . metrics . registered_requests . add ( 1 ) ;
143144 self . requests_by_offset . insert ( ( req. offset , req. id ) ) ;
144145 self . requests . insert ( req. id , req) ;
145146 }
@@ -149,20 +150,43 @@ impl State {
149150 self . requests_by_offset . remove ( & ( req. offset , req_id) ) ;
150151 tracing:: debug!( ?req, "ReadRequest dropped before poll" ) ;
151152 } else {
153+ self . metrics . polled_requests . add ( 1 ) ;
152154 self . polled_requests . insert ( req_id, req) ;
153155 }
154156 }
155157 }
156158 ReadEvent :: Dropped ( req_id) => {
157159 if let Some ( req) = self . requests . remove ( & req_id) {
160+ self . metrics . dropped_requests . add ( 1 ) ;
158161 self . requests_by_offset . remove ( & ( req. offset , req_id) ) ;
159162 tracing:: debug!( ?req, "ReadRequest dropped before poll" ) ;
160163 }
161164 if let Some ( req) = self . polled_requests . remove ( & req_id) {
165+ self . metrics . dropped_requests . add ( 1 ) ;
162166 self . requests_by_offset . remove ( & ( req. offset , req_id) ) ;
163167 tracing:: debug!( ?req, "ReadRequest dropped after poll" ) ;
164168 }
165169 }
170+ ReadEvent :: BatchBoundary => {
171+ // Promote all registered-but-unpolled requests to polled status.
172+ // This tells the coalescer that the entire batch is needed now,
173+ // allowing it to form optimal coalesced reads.
174+ let promoted = self . requests . len ( ) ;
175+ if promoted > 0 {
176+ tracing:: debug!(
177+ promoted,
178+ "BatchBoundary: promoting registered requests to polled"
179+ ) ;
180+ self . metrics . polled_requests . add ( promoted as u64 ) ;
181+ for ( req_id, req) in std:: mem:: take ( & mut self . requests ) {
182+ if req. callback . is_closed ( ) {
183+ self . requests_by_offset . remove ( & ( req. offset , req_id) ) ;
184+ } else {
185+ self . polled_requests . insert ( req_id, req) ;
186+ }
187+ }
188+ }
189+ }
166190 }
167191 }
168192
@@ -215,6 +239,9 @@ impl State {
215239 let first_req = self . next_uncoalesced ( ) ?;
216240
217241 let mut requests = vec ! [ first_req] ;
242+ let mut payload_bytes = requests[ 0 ] . length as u64 ;
243+ let mut registered_only_requests = 0usize ;
244+ let mut polled_requests = 1usize ;
218245 let mut current_start = requests[ 0 ] . offset ;
219246 let mut current_end = requests[ 0 ] . offset + requests[ 0 ] . length as u64 ;
220247 let align = * self . coalesced_buffer_alignment as u64 ;
@@ -269,18 +296,28 @@ impl State {
269296 let new_total_size = new_end - aligned_start;
270297
271298 if new_total_size > window. max_size {
299+ self . metrics . batched_skipped_max_size . add ( 1 ) ;
272300 // Skip it but keep it available for future coalescing operations.
273301 continue ;
274302 }
275303
276304 current_start = new_start;
277305 current_end = new_end;
278- let req = self
279- . polled_requests
280- . remove ( & req_id)
281- . or_else ( || self . requests . remove ( & req_id) )
282- . vortex_expect ( "Missing request in requests_by_offset" ) ;
306+ let ( req, was_polled) = if let Some ( req) = self . polled_requests . remove ( & req_id)
307+ {
308+ ( req, true )
309+ } else if let Some ( req) = self . requests . remove ( & req_id) {
310+ ( req, false )
311+ } else {
312+ unreachable ! ( "Missing request in requests_by_offset" ) ;
313+ } ;
283314
315+ payload_bytes = payload_bytes. saturating_add ( req. length as u64 ) ;
316+ if was_polled {
317+ polled_requests = polled_requests. saturating_add ( 1 ) ;
318+ } else {
319+ registered_only_requests = registered_only_requests. saturating_add ( 1 ) ;
320+ }
284321 requests. push ( req) ;
285322 if ids_to_remove. insert ( req_id) {
286323 keys_to_remove. push ( ( req_offset, req_id) ) ;
@@ -302,6 +339,18 @@ impl State {
302339 requests. sort_unstable_by_key ( |r| r. offset ) ;
303340
304341 let aligned_start = current_start - ( current_start % align) ;
342+ let range_bytes = current_end - aligned_start;
343+
344+ self . metrics . batched_range_bytes . update ( range_bytes as f64 ) ;
345+ self . metrics
346+ . batched_payload_bytes
347+ . update ( payload_bytes as f64 ) ;
348+ self . metrics
349+ . batched_registered_only_requests
350+ . update ( registered_only_requests as f64 ) ;
351+ self . metrics
352+ . batched_polled_requests
353+ . update ( polled_requests as f64 ) ;
305354
306355 tracing:: debug!(
307356 "Coalesced {} requests into range {}..{} (len={})" ,
@@ -808,4 +857,86 @@ mod tests {
808857 assert_eq ! ( individual_count, 2 , "Expected 2 individual requests" ) ;
809858 assert_eq ! ( coalesced_operations, 0 , "Expected 0 coalesced operations" ) ;
810859 }
860+
861+ #[ tokio:: test]
862+ async fn test_metrics_record_registered_only_batch_members ( ) {
863+ let ( req1, _rx1) = create_request ( 1 , 0 , 10 ) ;
864+ let ( req2, _rx2) = create_request ( 2 , 50 , 10 ) ;
865+ let ( req3, _rx3) = create_request ( 3 , 100 , 10 ) ;
866+
867+ let events = vec ! [
868+ ReadEvent :: Request ( req1) ,
869+ ReadEvent :: Request ( req2) ,
870+ ReadEvent :: Request ( req3) ,
871+ ReadEvent :: Polled ( 2 ) ,
872+ ] ;
873+
874+ let event_stream = stream:: iter ( events) ;
875+ let metrics_registry = DefaultMetricsRegistry :: default ( ) ;
876+ let metrics = RequestMetrics :: new ( & metrics_registry, vec ! [ ] ) ;
877+ let io_stream = IoRequestStream :: new (
878+ event_stream,
879+ Some ( CoalesceConfig {
880+ distance : 60 ,
881+ max_size : 1024 ,
882+ } ) ,
883+ Alignment :: none ( ) ,
884+ metrics,
885+ ) ;
886+
887+ let outputs: Vec < IoRequest > = io_stream. collect ( ) . await ;
888+ assert_eq ! ( outputs. len( ) , 1 ) ;
889+
890+ let snapshot = metrics_registry. snapshot ( ) ;
891+ let mut registered = 0u64 ;
892+ let mut polled = 0u64 ;
893+ let mut coalesced = 0u64 ;
894+ let mut registered_only_count = 0usize ;
895+ let mut registered_only_total = 0.0 ;
896+ let mut polled_in_batch_count = 0usize ;
897+ let mut polled_in_batch_total = 0.0 ;
898+
899+ for metric in snapshot. iter ( ) {
900+ match metric. value ( ) {
901+ MetricValue :: Counter ( counter) => match metric. name ( ) . as_ref ( ) {
902+ "io.requests.registered" => registered = counter. value ( ) ,
903+ "io.requests.polled" => polled = counter. value ( ) ,
904+ "io.requests.coalesced" => coalesced = counter. value ( ) ,
905+ _ => { }
906+ } ,
907+ MetricValue :: Histogram ( histogram) => match metric. name ( ) . as_ref ( ) {
908+ "io.requests.batched.registered_only_requests" => {
909+ registered_only_count = histogram. count ( ) ;
910+ registered_only_total = histogram. total ( ) ;
911+ }
912+ "io.requests.batched.polled_requests" => {
913+ polled_in_batch_count = histogram. count ( ) ;
914+ polled_in_batch_total = histogram. total ( ) ;
915+ }
916+ _ => { }
917+ } ,
918+ _ => { }
919+ }
920+ }
921+
922+ assert_eq ! ( registered, 3 , "Expected 3 registered requests" ) ;
923+ assert_eq ! ( polled, 1 , "Expected 1 polled request" ) ;
924+ assert_eq ! ( coalesced, 1 , "Expected 1 coalesced operation" ) ;
925+ assert_eq ! (
926+ registered_only_count, 1 ,
927+ "Expected one histogram sample for registered-only requests"
928+ ) ;
929+ assert_eq ! (
930+ registered_only_total, 2.0 ,
931+ "Expected two registered-only requests in the coalesced batch"
932+ ) ;
933+ assert_eq ! (
934+ polled_in_batch_count, 1 ,
935+ "Expected one histogram sample for polled requests"
936+ ) ;
937+ assert_eq ! (
938+ polled_in_batch_total, 1.0 ,
939+ "Expected one polled request in the coalesced batch"
940+ ) ;
941+ }
811942}
0 commit comments