fraiseql_wire/connection/conn/core.rs
1//! Core `Connection` type and implementation
2
3use super::config::ConnectionConfig;
4use super::helpers::extract_entity_from_query;
5use crate::auth::ScramClient;
6use crate::connection::state::ConnectionState;
7use crate::connection::transport::Transport;
8use crate::protocol::{
9 decode_message, encode_message, AuthenticationMessage, BackendMessage, FrontendMessage,
10};
11use crate::{Result, WireError};
12use bytes::{Buf, BytesMut};
13use std::sync::atomic::{AtomicU64, Ordering};
14use tracing::Instrument;
15
16// Global counter for chunk metrics sampling (1 per 10 chunks)
17// Used to reduce per-chunk metric recording overhead
18static CHUNK_COUNT: AtomicU64 = AtomicU64::new(0);
19
20/// Postgres connection
21pub struct Connection {
22 transport: Transport,
23 state: ConnectionState,
24 read_buf: BytesMut,
25 process_id: Option<i32>,
26 secret_key: Option<i32>,
27}
28
29impl Connection {
30 /// Create connection from transport
31 pub fn new(transport: Transport) -> Self {
32 Self {
33 transport,
34 state: ConnectionState::Initial,
35 read_buf: BytesMut::with_capacity(8192),
36 process_id: None,
37 secret_key: None,
38 }
39 }
40
41 /// Get current connection state
42 pub const fn state(&self) -> ConnectionState {
43 self.state
44 }
45
46 /// Perform startup and authentication
47 ///
48 /// # Errors
49 ///
50 /// Returns [`WireError::InvalidState`] if the connection is not in the `Initial` state.
51 /// Returns [`WireError::Authentication`] if authentication is rejected by the server.
52 /// Returns [`WireError`] on any I/O or protocol error during the handshake.
53 pub async fn startup(&mut self, config: &ConnectionConfig) -> Result<()> {
54 async {
55 self.state.transition(ConnectionState::AwaitingAuth)?;
56
57 // Build startup parameters
58 let mut params = vec![
59 ("user".to_string(), config.user.clone()),
60 ("database".to_string(), config.database.clone()),
61 ];
62
63 // Add configured application name if specified
64 if let Some(app_name) = &config.application_name {
65 params.push(("application_name".to_string(), app_name.clone()));
66 }
67
68 // Add statement timeout if specified (in milliseconds)
69 if let Some(timeout) = config.statement_timeout {
70 params.push((
71 "statement_timeout".to_string(),
72 timeout.as_millis().to_string(),
73 ));
74 }
75
76 // Add extra_float_digits if specified
77 if let Some(digits) = config.extra_float_digits {
78 params.push(("extra_float_digits".to_string(), digits.to_string()));
79 }
80
81 // Add user-provided parameters
82 for (k, v) in &config.params {
83 params.push((k.clone(), v.clone()));
84 }
85
86 // Send startup message
87 let startup = FrontendMessage::Startup {
88 version: crate::protocol::constants::PROTOCOL_VERSION,
89 params,
90 };
91 self.send_message(&startup).await?;
92
93 // Authentication loop
94 self.state.transition(ConnectionState::Authenticating)?;
95 self.authenticate(config).await?;
96
97 self.state.transition(ConnectionState::Idle)?;
98 tracing::info!("startup complete");
99 Ok(())
100 }
101 .instrument(tracing::info_span!(
102 "startup",
103 user = %config.user,
104 database = %config.database
105 ))
106 .await
107 }
108
109 /// Handle authentication
110 async fn authenticate(&mut self, config: &ConnectionConfig) -> Result<()> {
111 let auth_start = std::time::Instant::now();
112 let mut auth_mechanism = "unknown";
113
114 loop {
115 let msg = self.receive_message().await?;
116
117 match msg {
118 BackendMessage::Authentication(auth) => match auth {
119 AuthenticationMessage::Ok => {
120 tracing::debug!("authentication successful");
121 crate::metrics::counters::auth_successful(auth_mechanism);
122 crate::metrics::histograms::auth_duration(
123 auth_mechanism,
124 auth_start.elapsed().as_millis() as u64,
125 );
126 // Don't break here! Must continue reading until ReadyForQuery
127 }
128 AuthenticationMessage::CleartextPassword => {
129 auth_mechanism = crate::metrics::labels::MECHANISM_CLEARTEXT;
130 crate::metrics::counters::auth_attempted(auth_mechanism);
131
132 let password = config
133 .password
134 .as_ref()
135 .ok_or_else(|| WireError::Authentication("password required".into()))?;
136 // SECURITY: Convert from Zeroizing wrapper while preserving password content
137 let pwd_msg = FrontendMessage::Password(password.as_str().to_string());
138 self.send_message(&pwd_msg).await?;
139 }
140 AuthenticationMessage::Md5Password { .. } => {
141 return Err(WireError::Authentication(
142 "MD5 authentication not supported. Use SCRAM-SHA-256 or cleartext password".into(),
143 ));
144 }
145 AuthenticationMessage::Sasl { mechanisms } => {
146 auth_mechanism = crate::metrics::labels::MECHANISM_SCRAM;
147 crate::metrics::counters::auth_attempted(auth_mechanism);
148 self.handle_sasl(&mechanisms, config).await?;
149 }
150 AuthenticationMessage::SaslContinue { .. } => {
151 return Err(WireError::Protocol(
152 "unexpected SaslContinue outside of SASL flow".into(),
153 ));
154 }
155 AuthenticationMessage::SaslFinal { .. } => {
156 return Err(WireError::Protocol(
157 "unexpected SaslFinal outside of SASL flow".into(),
158 ));
159 }
160 },
161 BackendMessage::BackendKeyData {
162 process_id,
163 secret_key,
164 } => {
165 self.process_id = Some(process_id);
166 self.secret_key = Some(secret_key);
167 }
168 BackendMessage::ParameterStatus { name, value } => {
169 tracing::debug!("parameter status: {} = {}", name, value);
170 }
171 BackendMessage::ReadyForQuery { status: _ } => {
172 break;
173 }
174 BackendMessage::ErrorResponse(err) => {
175 crate::metrics::counters::auth_failed(auth_mechanism, "server_error");
176 return Err(WireError::Authentication(err.to_string()));
177 }
178 _ => {
179 return Err(WireError::Protocol(format!(
180 "unexpected message during auth: {:?}",
181 msg
182 )));
183 }
184 }
185 }
186
187 Ok(())
188 }
189
190 /// Handle SASL authentication (SCRAM-SHA-256)
191 async fn handle_sasl(
192 &mut self,
193 mechanisms: &[String],
194 config: &ConnectionConfig,
195 ) -> Result<()> {
196 // Check if server supports SCRAM-SHA-256
197 if !mechanisms.contains(&"SCRAM-SHA-256".to_string()) {
198 return Err(WireError::Authentication(format!(
199 "server does not support SCRAM-SHA-256. Available: {}",
200 mechanisms.join(", ")
201 )));
202 }
203
204 // Get password
205 let password = config.password.as_ref().ok_or_else(|| {
206 WireError::Authentication("password required for SCRAM authentication".into())
207 })?;
208
209 // Create SCRAM client
210 // SECURITY: Convert from Zeroizing wrapper while preserving password content
211 let mut scram = ScramClient::new(config.user.clone(), password.as_str().to_string());
212 tracing::debug!("initiating SCRAM-SHA-256 authentication");
213
214 // Send SaslInitialResponse with client first message
215 let client_first = scram.client_first();
216 let msg = FrontendMessage::SaslInitialResponse {
217 mechanism: "SCRAM-SHA-256".to_string(),
218 data: client_first.into_bytes(),
219 };
220 self.send_message(&msg).await?;
221
222 // Receive SaslContinue with server first message
223 let server_first_msg = self.receive_message().await?;
224 let server_first_data = match server_first_msg {
225 BackendMessage::Authentication(AuthenticationMessage::SaslContinue { data }) => data,
226 BackendMessage::ErrorResponse(err) => {
227 return Err(WireError::Authentication(format!(
228 "SASL server error: {}",
229 err
230 )));
231 }
232 _ => {
233 return Err(WireError::Protocol(
234 "expected SaslContinue message during SASL authentication".into(),
235 ));
236 }
237 };
238
239 let server_first = String::from_utf8(server_first_data).map_err(|e| {
240 WireError::Authentication(format!("invalid UTF-8 in server first message: {}", e))
241 })?;
242
243 tracing::debug!("received SCRAM server first message");
244
245 // Generate client final message
246 let (client_final, scram_state) = scram
247 .client_final(&server_first)
248 .map_err(|e| WireError::Authentication(format!("SCRAM error: {}", e)))?;
249
250 // Send SaslResponse with client final message
251 let msg = FrontendMessage::SaslResponse {
252 data: client_final.into_bytes(),
253 };
254 self.send_message(&msg).await?;
255
256 // Receive SaslFinal with server verification
257 let server_final_msg = self.receive_message().await?;
258 let server_final_data = match server_final_msg {
259 BackendMessage::Authentication(AuthenticationMessage::SaslFinal { data }) => data,
260 BackendMessage::ErrorResponse(err) => {
261 return Err(WireError::Authentication(format!(
262 "SASL server error: {}",
263 err
264 )));
265 }
266 _ => {
267 return Err(WireError::Protocol(
268 "expected SaslFinal message during SASL authentication".into(),
269 ));
270 }
271 };
272
273 let server_final = String::from_utf8(server_final_data).map_err(|e| {
274 WireError::Authentication(format!("invalid UTF-8 in server final message: {}", e))
275 })?;
276
277 // Verify server signature
278 scram
279 .verify_server_final(&server_final, &scram_state)
280 .map_err(|e| WireError::Authentication(format!("SCRAM verification failed: {}", e)))?;
281
282 tracing::debug!("SCRAM-SHA-256 authentication successful");
283 Ok(())
284 }
285
286 /// Execute a simple query (returns all backend messages)
287 ///
288 /// # Errors
289 ///
290 /// Returns [`WireError::ConnectionBusy`] if the connection is not idle.
291 /// Returns [`WireError::InvalidState`] if the state machine transition fails.
292 /// Returns [`WireError`] on any I/O or protocol error during execution.
293 pub async fn simple_query(&mut self, query: &str) -> Result<Vec<BackendMessage>> {
294 if self.state != ConnectionState::Idle {
295 return Err(WireError::ConnectionBusy(format!(
296 "connection in state: {}",
297 self.state
298 )));
299 }
300
301 self.state.transition(ConnectionState::QueryInProgress)?;
302
303 let query_msg = FrontendMessage::Query(query.to_string());
304 self.send_message(&query_msg).await?;
305
306 self.state.transition(ConnectionState::ReadingResults)?;
307
308 let mut messages = Vec::new();
309
310 loop {
311 let msg = self.receive_message().await?;
312 let is_ready = matches!(msg, BackendMessage::ReadyForQuery { .. });
313 messages.push(msg);
314
315 if is_ready {
316 break;
317 }
318 }
319
320 self.state.transition(ConnectionState::Idle)?;
321 Ok(messages)
322 }
323
324 /// Send a frontend message
325 async fn send_message(&mut self, msg: &FrontendMessage) -> Result<()> {
326 let buf = encode_message(msg)?;
327 self.transport.write_all(&buf).await?;
328 self.transport.flush().await?;
329 Ok(())
330 }
331
332 /// Receive a backend message
333 async fn receive_message(&mut self) -> Result<BackendMessage> {
334 loop {
335 // Try to decode a message from buffer (without cloning!)
336 if let Ok((msg, consumed)) = decode_message(&mut self.read_buf) {
337 self.read_buf.advance(consumed);
338 return Ok(msg);
339 }
340
341 // Bound read-buffer growth: a single backend message may not exceed
342 // MAX_MESSAGE_LEN. If we have buffered more than that without
343 // decoding one, the peer is sending an oversized (or
344 // never-terminating) message — fail instead of buffering toward
345 // ~2 GiB (audit M-wire-msg-cap). The full malformed-vs-incomplete
346 // decode-error distinction lands in the wire-protocol phase (H42).
347 if self.read_buf.len() > crate::protocol::decode::MAX_MESSAGE_LEN {
348 return Err(WireError::Protocol(format!(
349 "backend message exceeds maximum length of {} bytes",
350 crate::protocol::decode::MAX_MESSAGE_LEN
351 )));
352 }
353
354 // Need more data
355 let n = self.transport.read_buf(&mut self.read_buf).await?;
356 if n == 0 {
357 return Err(WireError::ConnectionClosed);
358 }
359 }
360 }
361
362 /// Close the connection
363 ///
364 /// # Errors
365 ///
366 /// Returns [`WireError::InvalidState`] if the state machine transition to `Closed` fails.
367 /// Returns [`WireError`] if the transport shutdown fails.
368 pub async fn close(mut self) -> Result<()> {
369 self.state.transition(ConnectionState::Closed)?;
370 let _ = self.send_message(&FrontendMessage::Terminate).await;
371 self.transport.shutdown().await?;
372 Ok(())
373 }
374
375 /// Execute a streaming query
376 ///
377 /// Note: This method consumes the connection. The stream maintains the connection
378 /// internally. Once the stream is exhausted or dropped, the connection is closed.
379 ///
380 /// # Errors
381 ///
382 /// Returns `WireError::Io` if sending the query or reading the response fails.
383 /// Returns `WireError::Database` if the server returns an error response.
384 /// Returns `WireError::InvalidSchema` if the row description is not a single JSON column.
385 #[allow(clippy::too_many_arguments)] // Reason: streaming query requires all chunking parameters; a config struct would add allocation overhead
386 pub async fn streaming_query(
387 mut self,
388 query: &str,
389 chunk_size: usize,
390 max_memory: Option<usize>,
391 soft_limit_warn_threshold: Option<f32>,
392 soft_limit_fail_threshold: Option<f32>,
393 enable_adaptive_chunking: bool,
394 adaptive_min_chunk_size: Option<usize>,
395 adaptive_max_chunk_size: Option<usize>,
396 ) -> Result<crate::stream::JsonStream> {
397 async {
398 let startup_start = std::time::Instant::now();
399
400 use crate::json::validate_row_description;
401 use crate::stream::{extract_json_bytes, parse_json, AdaptiveChunking, ChunkingStrategy, JsonStream};
402 use serde_json::Value;
403 use tokio::sync::mpsc;
404
405 if self.state != ConnectionState::Idle {
406 return Err(WireError::ConnectionBusy(format!(
407 "connection in state: {}",
408 self.state
409 )));
410 }
411
412 self.state.transition(ConnectionState::QueryInProgress)?;
413
414 let query_msg = FrontendMessage::Query(query.to_string());
415 self.send_message(&query_msg).await?;
416
417 self.state.transition(ConnectionState::ReadingResults)?;
418
419 // Read RowDescription, but handle other messages that may come first
420 // (e.g., ParameterStatus, BackendKeyData, ErrorResponse, NoticeResponse)
421 let row_desc;
422 loop {
423 let msg = self.receive_message().await?;
424
425 match msg {
426 BackendMessage::ErrorResponse(err) => {
427 // Query failed - consume ReadyForQuery and return error
428 tracing::debug!("PostgreSQL error response: {}", err);
429 loop {
430 let msg = self.receive_message().await?;
431 if matches!(msg, BackendMessage::ReadyForQuery { .. }) {
432 break;
433 }
434 }
435 return Err(WireError::Sql(err.to_string()));
436 }
437 BackendMessage::BackendKeyData { process_id, secret_key: _ } => {
438 // This provides the key needed for cancel requests - store it and continue
439 tracing::debug!("PostgreSQL backend key data received: pid={}", process_id);
440 // Note: We would store this if we need to support cancellation
441 continue;
442 }
443 BackendMessage::ParameterStatus { .. } => {
444 // Parameter status changes are informational - skip them
445 tracing::debug!("PostgreSQL parameter status change received");
446 continue;
447 }
448 BackendMessage::NoticeResponse(notice) => {
449 // Notices are non-fatal warnings - skip them
450 tracing::debug!("PostgreSQL notice: {}", notice);
451 continue;
452 }
453 BackendMessage::RowDescription(_) => {
454 row_desc = msg;
455 break;
456 }
457 BackendMessage::ReadyForQuery { .. } => {
458 // Received ReadyForQuery without RowDescription
459 // This means the query didn't produce a result set
460 return Err(WireError::Protocol(
461 "no result set received from query - \
462 check that the entity name is correct and the table/view exists"
463 .into(),
464 ));
465 }
466 _ => {
467 return Err(WireError::Protocol(format!(
468 "unexpected message type in query response: {:?}",
469 msg
470 )));
471 }
472 }
473 }
474
475 validate_row_description(&row_desc)?;
476
477 // Record startup timing
478 let startup_duration = startup_start.elapsed().as_millis() as u64;
479 let entity = extract_entity_from_query(query).unwrap_or_else(|| "unknown".to_string());
480 crate::metrics::histograms::query_startup_duration(&entity, startup_duration);
481
482 // Create channels
483 let (result_tx, result_rx) = mpsc::channel::<Result<Value>>(chunk_size);
484 let (cancel_tx, mut cancel_rx) = mpsc::channel::<()>(1);
485
486 // Create stream instance first so we can clone its pause/resume signals
487 let entity_for_metrics = extract_entity_from_query(query).unwrap_or_else(|| "unknown".to_string());
488 let entity_for_stream = entity_for_metrics.clone(); // Clone for stream
489
490 let stream = JsonStream::new(
491 result_rx,
492 cancel_tx,
493 entity_for_stream,
494 max_memory,
495 soft_limit_warn_threshold,
496 soft_limit_fail_threshold,
497 );
498
499 // Clone pause/resume signals for background task (only if pause/resume is initialized)
500 let state_lock = stream.clone_state();
501 let pause_signal = stream.clone_pause_signal();
502 let resume_signal = stream.clone_resume_signal();
503
504 // Clone atomic state for fast state checks in background task
505 let state_atomic = stream.clone_state_atomic();
506
507 // Clone pause timeout for background task
508 let pause_timeout = stream.pause_timeout();
509
510 // Spawn background task to read rows
511 let query_start = std::time::Instant::now();
512
513 tokio::spawn(async move {
514 let strategy = ChunkingStrategy::new(chunk_size);
515 let mut chunk = strategy.new_chunk();
516 let mut total_rows = 0u64;
517
518 // Initialize adaptive chunking if enabled
519 let _adaptive = if enable_adaptive_chunking {
520 let mut adp = AdaptiveChunking::new();
521
522 // Apply custom bounds if provided
523 if let Some(min) = adaptive_min_chunk_size {
524 if let Some(max) = adaptive_max_chunk_size {
525 adp = adp.with_bounds(min, max);
526 }
527 }
528
529 Some(adp)
530 } else {
531 None
532 };
533 let _current_chunk_size = chunk_size;
534
535 loop {
536 // Check lightweight atomic state first (fast path)
537 // Only check atomic if pause/resume infrastructure is actually initialized
538 if state_lock.is_some() && state_atomic.load(std::sync::atomic::Ordering::Acquire) == 1 {
539 // Paused state detected via atomic, now handle with Mutex
540 if let (Some(ref state_lock), Some(ref _pause_signal), Some(ref resume_signal)) =
541 (&state_lock, &pause_signal, &resume_signal)
542 {
543 let current_state = state_lock.lock().await;
544 if *current_state == crate::stream::StreamState::Paused {
545 tracing::debug!("stream paused, waiting for resume");
546 drop(current_state); // Release lock before waiting
547
548 // Wait with optional timeout
549 if let Some(timeout) = pause_timeout {
550 if tokio::time::timeout(timeout, resume_signal.notified()).await == Ok(()) {
551 tracing::debug!("stream resumed");
552 } else {
553 tracing::debug!("pause timeout expired, auto-resuming");
554 crate::metrics::counters::stream_pause_timeout_expired(&entity_for_metrics);
555 }
556 } else {
557 // No timeout, wait indefinitely
558 resume_signal.notified().await;
559 tracing::debug!("stream resumed");
560 }
561
562 // Update state back to Running
563 let mut state = state_lock.lock().await;
564 *state = crate::stream::StreamState::Running;
565 }
566 }
567 }
568
569 tokio::select! {
570 // Check for cancellation
571 _ = cancel_rx.recv() => {
572 tracing::debug!("query cancelled");
573 crate::metrics::counters::query_completed("cancelled", &entity_for_metrics);
574 break;
575 }
576
577 // Read next message
578 msg_result = self.receive_message() => {
579 match msg_result {
580 Ok(msg) => match msg {
581 BackendMessage::DataRow(_) => {
582 match extract_json_bytes(&msg) {
583 Ok(json_bytes) => {
584 chunk.push(json_bytes);
585
586 if strategy.is_full(&chunk) {
587 let chunk_start = std::time::Instant::now();
588 let rows = chunk.into_rows();
589 let chunk_size_rows = rows.len() as u64;
590
591 // Batch JSON parsing and sending to reduce lock contention
592 // Send 8 values per channel send instead of 1 (8x fewer locks)
593 const BATCH_SIZE: usize = 8;
594 let mut batch = Vec::with_capacity(BATCH_SIZE);
595 let mut send_error = false;
596
597 for row_bytes in rows {
598 match parse_json(row_bytes) {
599 Ok(value) => {
600 total_rows += 1;
601 batch.push(Ok(value));
602
603 // Send batch when full
604 if batch.len() == BATCH_SIZE {
605 for item in batch.drain(..) {
606 if result_tx.send(item).await.is_err() {
607 crate::metrics::counters::query_completed("error", &entity_for_metrics);
608 send_error = true;
609 break;
610 }
611 }
612 if send_error {
613 break;
614 }
615 }
616 }
617 Err(e) => {
618 crate::metrics::counters::json_parse_error(&entity_for_metrics);
619 let _ = result_tx.send(Err(e)).await;
620 crate::metrics::counters::query_completed("error", &entity_for_metrics);
621 send_error = true;
622 break;
623 }
624 }
625 }
626
627 // Send remaining batch items
628 if !send_error {
629 for item in batch {
630 if result_tx.send(item).await.is_err() {
631 crate::metrics::counters::query_completed("error", &entity_for_metrics);
632 break;
633 }
634 }
635 }
636
637 // Record chunk metrics (sampled, not per-chunk)
638 let chunk_duration = chunk_start.elapsed().as_millis() as u64;
639
640 // Only record metrics every 10 chunks to reduce overhead
641 let chunk_idx = CHUNK_COUNT.fetch_add(1, Ordering::Relaxed);
642 if chunk_idx.is_multiple_of(10) {
643 crate::metrics::histograms::chunk_processing_duration(&entity_for_metrics, chunk_duration);
644 crate::metrics::histograms::chunk_size(&entity_for_metrics, chunk_size_rows);
645 }
646
647 // Adaptive chunking: disabled by default for better performance
648 // Enable only if explicitly requested via enable_adaptive_chunking parameter
649 // Note: adaptive adjustment adds ~0.5-1% overhead per chunk
650 // For fixed chunk sizes (default), skip this entirely
651
652 chunk = strategy.new_chunk();
653 }
654 }
655 Err(e) => {
656 crate::metrics::counters::json_parse_error(&entity_for_metrics);
657 let _ = result_tx.send(Err(e)).await;
658 crate::metrics::counters::query_completed("error", &entity_for_metrics);
659 break;
660 }
661 }
662 }
663 BackendMessage::CommandComplete(_) => {
664 // Send remaining chunk
665 if !chunk.is_empty() {
666 let chunk_start = std::time::Instant::now();
667 let rows = chunk.into_rows();
668 let chunk_size_rows = rows.len() as u64;
669
670 // Batch JSON parsing and sending to reduce lock contention
671 const BATCH_SIZE: usize = 8;
672 let mut batch = Vec::with_capacity(BATCH_SIZE);
673 let mut send_error = false;
674
675 for row_bytes in rows {
676 match parse_json(row_bytes) {
677 Ok(value) => {
678 total_rows += 1;
679 batch.push(Ok(value));
680
681 // Send batch when full
682 if batch.len() == BATCH_SIZE {
683 for item in batch.drain(..) {
684 if result_tx.send(item).await.is_err() {
685 crate::metrics::counters::query_completed("error", &entity_for_metrics);
686 send_error = true;
687 break;
688 }
689 }
690 if send_error {
691 break;
692 }
693 }
694 }
695 Err(e) => {
696 crate::metrics::counters::json_parse_error(&entity_for_metrics);
697 let _ = result_tx.send(Err(e)).await;
698 crate::metrics::counters::query_completed("error", &entity_for_metrics);
699 send_error = true;
700 break;
701 }
702 }
703 }
704
705 // Send remaining batch items
706 if !send_error {
707 for item in batch {
708 if result_tx.send(item).await.is_err() {
709 crate::metrics::counters::query_completed("error", &entity_for_metrics);
710 break;
711 }
712 }
713 }
714
715 // Record final chunk metrics (sampled)
716 let chunk_duration = chunk_start.elapsed().as_millis() as u64;
717 let chunk_idx = CHUNK_COUNT.fetch_add(1, Ordering::Relaxed);
718 if chunk_idx.is_multiple_of(10) {
719 crate::metrics::histograms::chunk_processing_duration(&entity_for_metrics, chunk_duration);
720 crate::metrics::histograms::chunk_size(&entity_for_metrics, chunk_size_rows);
721 }
722 chunk = strategy.new_chunk();
723 }
724
725 // Record query completion metrics
726 let query_duration = query_start.elapsed().as_millis() as u64;
727 crate::metrics::counters::rows_processed(&entity_for_metrics, total_rows, "ok");
728 crate::metrics::histograms::query_total_duration(&entity_for_metrics, query_duration);
729 crate::metrics::counters::query_completed("success", &entity_for_metrics);
730 }
731 BackendMessage::ReadyForQuery { .. } => {
732 break;
733 }
734 BackendMessage::ErrorResponse(err) => {
735 crate::metrics::counters::query_error(&entity_for_metrics, "server_error");
736 crate::metrics::counters::query_completed("error", &entity_for_metrics);
737 let _ = result_tx.send(Err(WireError::Sql(err.to_string()))).await;
738 break;
739 }
740 _ => {
741 crate::metrics::counters::query_error(&entity_for_metrics, "protocol_error");
742 crate::metrics::counters::query_completed("error", &entity_for_metrics);
743 let _ = result_tx.send(Err(WireError::Protocol(
744 format!("unexpected message: {:?}", msg)
745 ))).await;
746 break;
747 }
748 },
749 Err(e) => {
750 crate::metrics::counters::query_error(&entity_for_metrics, "connection_error");
751 crate::metrics::counters::query_completed("error", &entity_for_metrics);
752 let _ = result_tx.send(Err(e)).await;
753 break;
754 }
755 }
756 }
757 }
758 }
759 });
760
761 Ok(stream)
762 }
763 .instrument(tracing::debug_span!(
764 "streaming_query",
765 query = %query,
766 chunk_size = %chunk_size
767 ))
768 .await
769 }
770}