fraiseql_wire/connection/conn/
core.rs1use 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
16static CHUNK_COUNT: AtomicU64 = AtomicU64::new(0);
19
20pub 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 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 pub const fn state(&self) -> ConnectionState {
43 self.state
44 }
45
46 pub async fn startup(&mut self, config: &ConnectionConfig) -> Result<()> {
54 async {
55 self.state.transition(ConnectionState::AwaitingAuth)?;
56
57 let mut params = vec![
59 ("user".to_string(), config.user.clone()),
60 ("database".to_string(), config.database.clone()),
61 ];
62
63 if let Some(app_name) = &config.application_name {
65 params.push(("application_name".to_string(), app_name.clone()));
66 }
67
68 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 if let Some(digits) = config.extra_float_digits {
78 params.push(("extra_float_digits".to_string(), digits.to_string()));
79 }
80
81 for (k, v) in &config.params {
83 params.push((k.clone(), v.clone()));
84 }
85
86 let startup = FrontendMessage::Startup {
88 version: crate::protocol::constants::PROTOCOL_VERSION,
89 params,
90 };
91 self.send_message(&startup).await?;
92
93 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 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 }
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 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 async fn handle_sasl(
192 &mut self,
193 mechanisms: &[String],
194 config: &ConnectionConfig,
195 ) -> Result<()> {
196 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 let password = config.password.as_ref().ok_or_else(|| {
206 WireError::Authentication("password required for SCRAM authentication".into())
207 })?;
208
209 let mut scram = ScramClient::new(config.user.clone(), password.as_str().to_string());
212 tracing::debug!("initiating SCRAM-SHA-256 authentication");
213
214 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 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 let (client_final, scram_state) = scram
247 .client_final(&server_first)
248 .map_err(|e| WireError::Authentication(format!("SCRAM error: {}", e)))?;
249
250 let msg = FrontendMessage::SaslResponse {
252 data: client_final.into_bytes(),
253 };
254 self.send_message(&msg).await?;
255
256 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 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 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 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 async fn receive_message(&mut self) -> Result<BackendMessage> {
343 loop {
344 match decode_message(&mut self.read_buf) {
346 Ok((msg, consumed)) => {
347 self.read_buf.advance(consumed);
348 return Ok(msg);
349 }
350 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {}
352 Err(e) => {
354 crate::metrics::counters::protocol_error("decode");
355 return Err(WireError::Protocol(format!(
356 "failed to decode backend message: {e}"
357 )));
358 }
359 }
360
361 if self.read_buf.len() > crate::protocol::decode::MAX_MESSAGE_LEN {
369 return Err(WireError::Protocol(format!(
370 "backend message exceeds maximum length of {} bytes",
371 crate::protocol::decode::MAX_MESSAGE_LEN
372 )));
373 }
374
375 let n = self.transport.read_buf(&mut self.read_buf).await?;
377 if n == 0 {
378 return Err(WireError::ConnectionClosed);
379 }
380 }
381 }
382
383 pub async fn close(mut self) -> Result<()> {
390 self.state.transition(ConnectionState::Closed)?;
391 let _ = self.send_message(&FrontendMessage::Terminate).await;
392 self.transport.shutdown().await?;
393 Ok(())
394 }
395
396 #[allow(clippy::too_many_arguments)] pub async fn streaming_query(
408 mut self,
409 query: &str,
410 chunk_size: usize,
411 max_memory: Option<usize>,
412 soft_limit_warn_threshold: Option<f32>,
413 soft_limit_fail_threshold: Option<f32>,
414 enable_adaptive_chunking: bool,
415 adaptive_min_chunk_size: Option<usize>,
416 adaptive_max_chunk_size: Option<usize>,
417 ) -> Result<crate::stream::JsonStream> {
418 async {
419 let startup_start = std::time::Instant::now();
420
421 use crate::json::validate_row_description;
422 use crate::stream::{extract_json_bytes, AdaptiveChunking, ChunkingStrategy, JsonStream};
423 use serde_json::Value;
424 use tokio::sync::mpsc;
425
426 if self.state != ConnectionState::Idle {
427 return Err(WireError::ConnectionBusy(format!(
428 "connection in state: {}",
429 self.state
430 )));
431 }
432
433 self.state.transition(ConnectionState::QueryInProgress)?;
434
435 let query_msg = FrontendMessage::Query(query.to_string());
436 self.send_message(&query_msg).await?;
437
438 self.state.transition(ConnectionState::ReadingResults)?;
439
440 let row_desc;
443 loop {
444 let msg = self.receive_message().await?;
445
446 match msg {
447 BackendMessage::ErrorResponse(err) => {
448 tracing::debug!("PostgreSQL error response: {}", err);
450 loop {
451 let msg = self.receive_message().await?;
452 if matches!(msg, BackendMessage::ReadyForQuery { .. }) {
453 break;
454 }
455 }
456 return Err(WireError::Sql(err.to_string()));
457 }
458 BackendMessage::BackendKeyData { process_id, secret_key: _ } => {
459 tracing::debug!("PostgreSQL backend key data received: pid={}", process_id);
461 continue;
463 }
464 BackendMessage::ParameterStatus { .. } => {
465 tracing::debug!("PostgreSQL parameter status change received");
467 continue;
468 }
469 BackendMessage::NoticeResponse(notice) => {
470 tracing::debug!("PostgreSQL notice: {}", notice);
472 continue;
473 }
474 BackendMessage::RowDescription(_) => {
475 row_desc = msg;
476 break;
477 }
478 BackendMessage::ReadyForQuery { .. } => {
479 return Err(WireError::Protocol(
482 "no result set received from query - \
483 check that the entity name is correct and the table/view exists"
484 .into(),
485 ));
486 }
487 _ => {
488 return Err(WireError::Protocol(format!(
489 "unexpected message type in query response: {:?}",
490 msg
491 )));
492 }
493 }
494 }
495
496 validate_row_description(&row_desc)?;
497
498 let startup_duration = startup_start.elapsed().as_millis() as u64;
500 let entity = extract_entity_from_query(query).unwrap_or_else(|| "unknown".to_string());
501 crate::metrics::histograms::query_startup_duration(&entity, startup_duration);
502
503 let (result_tx, result_rx) = mpsc::channel::<Result<Value>>(chunk_size);
505 let (cancel_tx, mut cancel_rx) = mpsc::channel::<()>(1);
506
507 let entity_for_metrics = extract_entity_from_query(query).unwrap_or_else(|| "unknown".to_string());
509 let entity_for_stream = entity_for_metrics.clone(); let stream = JsonStream::new(
512 result_rx,
513 cancel_tx,
514 entity_for_stream,
515 max_memory,
516 soft_limit_warn_threshold,
517 soft_limit_fail_threshold,
518 );
519
520 let state_lock = stream.clone_state();
525 let resume_signal = stream.clone_resume_signal();
526
527 let state_atomic = stream.clone_state_atomic();
529
530 let pause_timeout_ms = stream.clone_pause_timeout();
533
534 let query_start = std::time::Instant::now();
536
537 tokio::spawn(async move {
538 let mut strategy = ChunkingStrategy::new(chunk_size);
539 let mut chunk = strategy.new_chunk();
540 let mut total_rows = 0u64;
541
542 let mut adaptive = if enable_adaptive_chunking {
547 let mut adp = AdaptiveChunking::new();
548 if let (Some(min), Some(max)) = (adaptive_min_chunk_size, adaptive_max_chunk_size) {
549 adp = adp.with_bounds(min, max);
550 }
551 Some(adp)
552 } else {
553 None
554 };
555 let mut current_chunk_size = chunk_size;
556
557 loop {
558 if state_atomic.load(std::sync::atomic::Ordering::Acquire) == 1 {
561 let is_paused =
562 { *state_lock.lock().await == crate::stream::StreamState::Paused };
563 if is_paused {
564 tracing::debug!("stream paused, waiting for resume");
565
566 let timeout_ms = pause_timeout_ms.load(std::sync::atomic::Ordering::Relaxed);
571 let cancelled = if timeout_ms > 0 {
572 tokio::select! {
573 () = resume_signal.notified() => {
574 tracing::debug!("stream resumed");
575 false
576 }
577 _ = cancel_rx.recv() => true,
578 () = tokio::time::sleep(std::time::Duration::from_millis(timeout_ms)) => {
579 tracing::debug!("pause timeout expired, auto-resuming");
580 crate::metrics::counters::stream_pause_timeout_expired(&entity_for_metrics);
581 false
582 }
583 }
584 } else {
585 tokio::select! {
586 () = resume_signal.notified() => {
587 tracing::debug!("stream resumed");
588 false
589 }
590 _ = cancel_rx.recv() => true,
591 }
592 };
593
594 if cancelled {
595 tracing::debug!("query cancelled while paused");
596 crate::metrics::counters::query_completed("cancelled", &entity_for_metrics);
597 break;
598 }
599
600 *state_lock.lock().await = crate::stream::StreamState::Running;
603 state_atomic.store(0, std::sync::atomic::Ordering::Release);
604 }
605 }
606
607 tokio::select! {
608 _ = cancel_rx.recv() => {
610 tracing::debug!("query cancelled");
611 crate::metrics::counters::query_completed("cancelled", &entity_for_metrics);
612 break;
613 }
614
615 msg_result = self.receive_message() => {
617 match msg_result {
618 Ok(msg) => match msg {
619 BackendMessage::DataRow(_) => {
620 match extract_json_bytes(&msg) {
621 Ok(json_bytes) => {
622 chunk.push(json_bytes);
623
624 if strategy.is_full(&chunk) {
625 let chunk_start = std::time::Instant::now();
626 let rows = chunk.into_rows();
627 let chunk_size_rows = rows.len() as u64;
628
629 let buffered = result_tx
631 .max_capacity()
632 .saturating_sub(result_tx.capacity());
633
634 if !stream_chunk_rows(
635 rows,
636 &result_tx,
637 &entity_for_metrics,
638 &mut total_rows,
639 )
640 .await
641 {
642 break;
643 }
644
645 record_chunk_metrics(
646 &entity_for_metrics,
647 chunk_start,
648 chunk_size_rows,
649 );
650
651 if let Some(adaptive) = adaptive.as_mut() {
654 if let Some(new_size) = adaptive
655 .observe(buffered, result_tx.max_capacity())
656 {
657 crate::metrics::counters::adaptive_chunk_adjusted(
658 &entity_for_metrics,
659 current_chunk_size,
660 new_size,
661 );
662 current_chunk_size = new_size;
663 strategy = ChunkingStrategy::new(new_size);
664 }
665 }
666
667 chunk = strategy.new_chunk();
668 }
669 }
670 Err(e) => {
671 crate::metrics::counters::json_parse_error(&entity_for_metrics);
672 let _ = result_tx.send(Err(e)).await;
673 crate::metrics::counters::query_completed("error", &entity_for_metrics);
674 break;
675 }
676 }
677 }
678 BackendMessage::CommandComplete(_) => {
679 if !chunk.is_empty() {
686 let chunk_start = std::time::Instant::now();
687 let rows = chunk.into_rows();
688 let chunk_size_rows = rows.len() as u64;
689 if !stream_chunk_rows(
690 rows,
691 &result_tx,
692 &entity_for_metrics,
693 &mut total_rows,
694 )
695 .await
696 {
697 break;
698 }
699 record_chunk_metrics(
700 &entity_for_metrics,
701 chunk_start,
702 chunk_size_rows,
703 );
704 chunk = strategy.new_chunk();
705 }
706
707 let query_duration = query_start.elapsed().as_millis() as u64;
709 crate::metrics::counters::rows_processed(&entity_for_metrics, total_rows, "ok");
710 crate::metrics::histograms::query_total_duration(&entity_for_metrics, query_duration);
711 crate::metrics::counters::query_completed("success", &entity_for_metrics);
712 }
713 BackendMessage::ReadyForQuery { .. } => {
714 break;
715 }
716 BackendMessage::ErrorResponse(err) => {
717 crate::metrics::counters::query_error(&entity_for_metrics, "server_error");
718 crate::metrics::counters::query_completed("error", &entity_for_metrics);
719 let _ = result_tx.send(Err(WireError::Sql(err.to_string()))).await;
720 break;
721 }
722 _ => {
723 crate::metrics::counters::query_error(&entity_for_metrics, "protocol_error");
724 crate::metrics::counters::query_completed("error", &entity_for_metrics);
725 let _ = result_tx.send(Err(WireError::Protocol(
726 format!("unexpected message: {:?}", msg)
727 ))).await;
728 break;
729 }
730 },
731 Err(e) => {
732 crate::metrics::counters::query_error(&entity_for_metrics, "connection_error");
733 crate::metrics::counters::query_completed("error", &entity_for_metrics);
734 let _ = result_tx.send(Err(e)).await;
735 break;
736 }
737 }
738 }
739 }
740 }
741 });
742
743 Ok(stream)
744 }
745 .instrument(tracing::debug_span!(
746 "streaming_query",
747 query = %query,
748 chunk_size = %chunk_size
749 ))
750 .await
751 }
752}
753
754async fn stream_chunk_rows(
763 rows: Vec<bytes::Bytes>,
764 result_tx: &tokio::sync::mpsc::Sender<Result<serde_json::Value>>,
765 entity: &str,
766 total_rows: &mut u64,
767) -> bool {
768 const BATCH_SIZE: usize = 8;
769 let mut batch: Vec<Result<serde_json::Value>> = Vec::with_capacity(BATCH_SIZE);
770
771 for row_bytes in rows {
772 match crate::stream::parse_json(row_bytes) {
773 Ok(value) => {
774 *total_rows += 1;
775 batch.push(Ok(value));
776 if batch.len() == BATCH_SIZE {
777 for item in batch.drain(..) {
778 if result_tx.send(item).await.is_err() {
779 crate::metrics::counters::query_completed("error", entity);
780 return false;
781 }
782 }
783 }
784 }
785 Err(e) => {
786 crate::metrics::counters::json_parse_error(entity);
787 let _ = result_tx.send(Err(e)).await;
788 crate::metrics::counters::query_completed("error", entity);
789 return false;
790 }
791 }
792 }
793
794 for item in batch {
796 if result_tx.send(item).await.is_err() {
797 crate::metrics::counters::query_completed("error", entity);
798 return false;
799 }
800 }
801 true
802}
803
804fn record_chunk_metrics(entity: &str, chunk_start: std::time::Instant, chunk_size_rows: u64) {
806 let chunk_duration = chunk_start.elapsed().as_millis() as u64;
807 let chunk_idx = CHUNK_COUNT.fetch_add(1, Ordering::Relaxed);
808 if chunk_idx.is_multiple_of(10) {
809 crate::metrics::histograms::chunk_processing_duration(entity, chunk_duration);
810 crate::metrics::histograms::chunk_size(entity, chunk_size_rows);
811 }
812}