Skip to main content

questdb/ingress/
sender.rs

1/*******************************************************************************
2 *     ___                  _   ____  ____
3 *    / _ \ _   _  ___  ___| |_|  _ \| __ )
4 *   | | | | | | |/ _ \/ __| __| | | |  _ \
5 *   | |_| | |_| |  __/\__ \ |_| |_| | |_) |
6 *    \__\_\\__,_|\___||___/\__|____/|____/
7 *
8 *  Copyright (c) 2014-2019 Appsicle
9 *  Copyright (c) 2019-2025 QuestDB
10 *
11 *  Licensed under the Apache License, Version 2.0 (the "License");
12 *  you may not use this file except in compliance with the License.
13 *  You may obtain a copy of the License at
14 *
15 *  http://www.apache.org/licenses/LICENSE-2.0
16 *
17 *  Unless required by applicable law or agreed to in writing, software
18 *  distributed under the License is distributed on an "AS IS" BASIS,
19 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 *  See the License for the specific language governing permissions and
21 *  limitations under the License.
22 *
23 ******************************************************************************/
24
25// `SyncProtocolHandler` is cfg-pruned: with only `sync-sender-qwp-ws`
26// enabled, the enum has just the two `*Ws` variants and a number
27// of `_ =>` fallbacks here become unreachable. Suppress only in that
28// exact configuration so a regression in the multi-handler builds
29// still surfaces.
30#![cfg_attr(
31    not(any(
32        feature = "sync-sender-tcp",
33        feature = "sync-sender-http",
34        feature = "sync-sender-qwp-udp"
35    )),
36    allow(unreachable_patterns)
37)]
38
39use crate::error::{self, Result};
40#[cfg(feature = "sync-sender-qwp-ws")]
41use crate::ingress::AckLevel;
42#[cfg(feature = "_sync-sender")]
43use crate::ingress::SenderBuilder;
44use crate::ingress::{Buffer, Protocol, ProtocolVersion};
45use std::fmt::{Debug, Formatter};
46#[cfg(feature = "sync-sender-qwp-ws")]
47use std::sync::atomic::{AtomicUsize, Ordering};
48#[cfg(feature = "sync-sender-qwp-ws")]
49use std::time::{Duration, Instant};
50
51#[cfg(feature = "sync-sender-qwp-udp")]
52mod qwp_udp;
53
54#[cfg(feature = "sync-sender-qwp-udp")]
55pub(crate) use qwp_udp::*;
56
57#[cfg(feature = "_sender-qwp-ws")]
58mod qwp_ws_codec;
59
60#[cfg(feature = "_sender-qwp-ws")]
61mod qwp_ws_driver;
62
63#[cfg(feature = "sync-sender-qwp-ws")]
64pub(crate) use qwp_ws_driver::{
65    ReconnectPolicy, ReconnectReason, reconnect_backoff_step, reconnect_error_is_terminal,
66};
67
68#[cfg(feature = "_sender-qwp-ws")]
69mod qwp_ws_ownership;
70
71#[cfg(feature = "_sender-qwp-ws")]
72mod qwp_ws_orphan;
73#[cfg(all(test, feature = "_sender-qwp-ws"))]
74pub(crate) use qwp_ws_orphan::has_any_sfa_file;
75#[cfg(feature = "_sender-qwp-ws")]
76pub(crate) use qwp_ws_orphan::is_candidate_orphan;
77
78#[cfg(feature = "_sender-qwp-ws")]
79mod qwp_ws_publisher;
80
81#[cfg(feature = "_sender-qwp-ws")]
82mod qwp_ws_queue;
83
84#[cfg(feature = "_sender-qwp-ws")]
85mod qwp_ws_sfa_segment;
86
87#[cfg(feature = "_sender-qwp-ws")]
88pub(crate) mod qwp_ws_sfa_manifest;
89
90#[cfg(feature = "_sender-qwp-ws")]
91mod qwp_ws_sfa_queue;
92
93#[cfg(feature = "_sender-qwp-ws")]
94mod qwp_ws_sfa_slot;
95
96#[cfg(feature = "_sender-qwp-ws")]
97pub(crate) mod qwp_ws_sfa_publisher;
98
99#[cfg(feature = "_sender-qwp-ws")]
100pub(crate) mod qwp_ws_sfa_symbol_dict;
101
102#[cfg(feature = "_sender-qwp-ws")]
103mod qwp_ws_sfa_catchup;
104
105#[cfg(all(test, feature = "_sender-qwp-ws"))]
106pub(crate) use qwp_ws_sfa_catchup::fail_next_catch_up_allocation_for_test;
107
108#[cfg(feature = "_sender-qwp-ws")]
109pub(crate) use qwp_ws_ownership::QwpWsRoleReject;
110#[cfg(feature = "_sender-qwp-ws")]
111pub use qwp_ws_ownership::*;
112
113#[cfg(feature = "sync-sender-qwp-ws")]
114pub(crate) mod qwp_ws;
115
116#[cfg(feature = "sync-sender-qwp-ws")]
117pub(crate) use qwp_ws::*;
118
119#[cfg(feature = "sync-sender-tcp")]
120mod tcp;
121
122#[cfg(feature = "sync-sender-tcp")]
123pub(crate) use tcp::*;
124
125#[cfg(feature = "sync-sender-tcp")]
126use std::io::Write;
127
128#[cfg(feature = "sync-sender-tcp")]
129use crate::ingress::map_io_to_socket_err;
130
131#[cfg(feature = "sync-sender-http")]
132mod http;
133
134#[cfg(feature = "sync-sender-http")]
135pub(crate) use http::*;
136
137#[cfg(feature = "sync-sender-qwp-ws")]
138fn effective_qwp_ws_max_buf_size(configured: usize, server_max: &AtomicUsize) -> usize {
139    let server = server_max.load(Ordering::Relaxed);
140    if server > 0 {
141        configured.min(server)
142    } else {
143        configured
144    }
145}
146
147#[allow(clippy::enum_variant_names)]
148pub(crate) enum SyncProtocolHandler {
149    #[cfg(feature = "sync-sender-qwp-udp")]
150    SyncQwpUdp(SyncQwpUdpHandlerState),
151
152    #[cfg(feature = "sync-sender-qwp-ws")]
153    SyncQwpWs(Box<SyncQwpWsHandlerState>),
154
155    #[cfg(feature = "sync-sender-qwp-ws")]
156    ManualQwpWs(Box<ManualQwpWsHandlerState>),
157
158    #[cfg(feature = "sync-sender-tcp")]
159    SyncTcp(SyncConnection),
160
161    #[cfg(feature = "sync-sender-http")]
162    SyncHttp(SyncHttpHandlerState),
163}
164
165/// Connects to a QuestDB instance and inserts data via the configured
166/// ingestion protocol.
167///
168/// * To construct an instance, use [`Sender::from_conf`] or the [`SenderBuilder`].
169/// * To prepare messages, use [`Buffer`] objects.
170/// * To send messages, call the [`flush`](Sender::flush) method.
171pub struct Sender {
172    descr: String,
173    handler: SyncProtocolHandler,
174    connected: bool,
175    init_buf_size: usize,
176    max_buf_size: usize,
177    protocol: Protocol,
178    protocol_version: ProtocolVersion,
179    max_name_len: usize,
180    #[cfg(feature = "_sender-qwp-ws")]
181    qwp_ws_error_handler: QwpWsErrorHandler,
182    #[cfg(feature = "_sender-qwp-ws")]
183    conn_events: Option<std::sync::Arc<crate::ingress::conn_events::ConnectionEventSource>>,
184}
185
186impl Debug for Sender {
187    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
188        f.write_str(self.descr.as_str())
189    }
190}
191
192impl Sender {
193    /// Total connection events discarded by the listener inbox's
194    /// drop-oldest policy. `0` when no listener is registered.
195    #[cfg(feature = "_sender-qwp-ws")]
196    pub fn connection_events_dropped(&self) -> u64 {
197        self.conn_events
198            .as_deref()
199            .map(|events| events.dropped())
200            .unwrap_or(0)
201    }
202
203    /// Total connection events delivered to the listener. `0` when no
204    /// listener is registered.
205    #[cfg(feature = "_sender-qwp-ws")]
206    pub fn connection_events_delivered(&self) -> u64 {
207        self.conn_events
208            .as_deref()
209            .map(|events| events.delivered())
210            .unwrap_or(0)
211    }
212
213    #[allow(clippy::too_many_arguments)]
214    pub(crate) fn new(
215        descr: String,
216        handler: SyncProtocolHandler,
217        init_buf_size: usize,
218        max_buf_size: usize,
219        protocol: Protocol,
220        protocol_version: ProtocolVersion,
221        max_name_len: usize,
222        #[cfg(feature = "_sender-qwp-ws")] qwp_ws_error_handler: QwpWsErrorHandler,
223        #[cfg(feature = "_sender-qwp-ws")] conn_events: Option<
224            std::sync::Arc<crate::ingress::conn_events::ConnectionEventSource>,
225        >,
226    ) -> Self {
227        Self {
228            descr,
229            handler,
230            connected: true,
231            init_buf_size,
232            max_buf_size,
233            protocol,
234            protocol_version,
235            max_name_len,
236            #[cfg(feature = "_sender-qwp-ws")]
237            qwp_ws_error_handler,
238            #[cfg(feature = "_sender-qwp-ws")]
239            conn_events,
240        }
241    }
242
243    /// Create a new `Sender` instance from the given configuration string.
244    ///
245    /// The format of the string is: `"http::addr=host:port;key=value;...;"`.
246    ///
247    /// Instead of `"http"`, you can also specify `"https"`, `"tcp"`, `"tcps"`,
248    /// and `"udp"`.
249    ///
250    /// We recommend HTTP for most cases because it provides more features, like
251    /// reporting errors to the client and supporting transaction control. TCP can
252    /// sometimes be faster in higher-latency networks, but misses a number of
253    /// features.
254    ///
255    /// Keys in the config string correspond to same-named methods on `SenderBuilder`.
256    ///
257    /// For the full list of keys and values, see the docs on [`SenderBuilder`].
258    ///
259    /// You can also load the configuration from an environment variable.
260    /// See [`Sender::from_env`].
261    ///
262    /// In the case of TCP, this synchronously establishes the TCP connection, and
263    /// returns once the connection is fully established. If the connection
264    /// requires authentication or TLS, these will also be completed before
265    /// returning.
266    #[cfg(feature = "_sync-sender")]
267    pub fn from_conf<T: AsRef<str>>(conf: T) -> Result<Self> {
268        SenderBuilder::from_conf(conf)?.build()
269    }
270
271    /// Create a new `Sender` from the configuration stored in the `QDB_CLIENT_CONF`
272    /// environment variable. The format is the same as that accepted by
273    /// [`Sender::from_conf`].
274    ///
275    /// In the case of TCP, this synchronously establishes the TCP connection, and
276    /// returns once the connection is fully established. If the connection
277    /// requires authentication or TLS, these will also be completed before
278    /// returning.
279    #[cfg(feature = "_sync-sender")]
280    pub fn from_env() -> Result<Self> {
281        SenderBuilder::from_env()?.build()
282    }
283
284    /// Creates a new [`Buffer`] using the sender's protocol settings
285    pub fn new_buffer(&self) -> Buffer {
286        #[cfg(feature = "sync-sender-qwp-udp")]
287        if matches!(&self.handler, SyncProtocolHandler::SyncQwpUdp(_)) {
288            return Buffer::qwp_with_max_name_len(self.max_name_len);
289        }
290
291        #[cfg(feature = "sync-sender-qwp-ws")]
292        if matches!(
293            &self.handler,
294            SyncProtocolHandler::SyncQwpWs(_) | SyncProtocolHandler::ManualQwpWs(_)
295        ) {
296            return Buffer::qwp_ws_with_max_name_len(self.max_name_len);
297        }
298
299        Buffer::with_init_capacity_and_max_name_len(
300            self.protocol_version,
301            self.init_buf_size,
302            self.max_name_len,
303        )
304    }
305
306    #[cfg(feature = "sync-sender-qwp-ws")]
307    fn drain_qwp_ws_error_notifications(&mut self) -> Result<()> {
308        loop {
309            let error = match &mut self.handler {
310                SyncProtocolHandler::SyncQwpWs(state) => {
311                    qwp_ws_poll_sender_error_notification_background(state)?
312                }
313                SyncProtocolHandler::ManualQwpWs(state) => {
314                    qwp_ws_poll_sender_error_notification_manual(state)?
315                }
316                _ => return Ok(()),
317            };
318            let Some(error) = error else {
319                return Ok(());
320            };
321            self.qwp_ws_error_handler.handle(&error);
322        }
323    }
324
325    #[cfg(feature = "sync-sender-qwp-ws")]
326    fn flush_qwp_ws_buffer(&mut self, buf: &Buffer, transactional: bool) -> Result<Option<u64>> {
327        if !matches!(
328            &self.handler,
329            SyncProtocolHandler::SyncQwpWs(_) | SyncProtocolHandler::ManualQwpWs(_)
330        ) {
331            return Err(error::fmt!(
332                InvalidApiCall,
333                "QWP/WebSocket FSN methods are only supported for QWP/WebSocket senders."
334            ));
335        }
336        match &self.handler {
337            SyncProtocolHandler::SyncQwpWs(state) => {
338                if let Err(err) = qwp_ws_check_error_background(state) {
339                    let _ = self.drain_qwp_ws_error_notifications();
340                    return Err(err);
341                }
342            }
343            SyncProtocolHandler::ManualQwpWs(state) => {
344                if let Err(err) = qwp_ws_check_error_manual(state) {
345                    let _ = self.drain_qwp_ws_error_notifications();
346                    return Err(err);
347                }
348            }
349            _ => unreachable!("QWP/WebSocket handler was checked above"),
350        }
351        self.drain_qwp_ws_error_notifications()?;
352
353        let qwp = buf.as_qwp_ws().ok_or_else(|| {
354            error::fmt!(
355                InvalidApiCall,
356                "QWP/WebSocket sender requires a QWP/WebSocket buffer created by `Sender::new_buffer()`."
357            )
358        })?;
359        qwp.check_can_flush()?;
360        if qwp.is_empty() {
361            return Ok(None);
362        }
363        if transactional {
364            return Err(error::fmt!(
365                InvalidApiCall,
366                "Transactional flushes are not supported for QWP/WebSocket."
367            ));
368        }
369
370        let result = match &mut self.handler {
371            SyncProtocolHandler::SyncQwpWs(state) => {
372                let max =
373                    effective_qwp_ws_max_buf_size(self.max_buf_size, &state.server_max_batch_size);
374                flush_qwp_ws(state, qwp, max)
375            }
376            SyncProtocolHandler::ManualQwpWs(state) => {
377                let max =
378                    effective_qwp_ws_max_buf_size(self.max_buf_size, &state.server_max_batch_size);
379                flush_qwp_ws_manual(state, qwp, max)
380            }
381            _ => unreachable!("QWP/WebSocket handler was checked above"),
382        };
383        if result
384            .as_ref()
385            .is_err_and(|err| matches!(err.code(), crate::ErrorCode::SocketError))
386        {
387            self.connected = false;
388        }
389        result
390    }
391
392    #[allow(unused_variables)]
393    fn flush_impl(&mut self, buf: &Buffer, transactional: bool) -> Result<()> {
394        #[cfg(feature = "sync-sender-qwp-udp")]
395        #[allow(irrefutable_let_patterns)]
396        if let SyncProtocolHandler::SyncQwpUdp(ref mut state) = self.handler {
397            let qwp = buf.as_qwp().ok_or_else(|| {
398                error::fmt!(
399                    InvalidApiCall,
400                    "QWP/UDP sender requires a QWP buffer created by `Sender::new_buffer()`."
401                )
402            })?;
403            qwp.check_can_flush()?;
404            if qwp.is_empty() {
405                return Ok(());
406            }
407            if qwp.len() > self.max_buf_size {
408                return Err(error::fmt!(
409                    InvalidApiCall,
410                    "Could not flush buffer: QWP buffer size hint of {} exceeds maximum configured allowed size of {} bytes.",
411                    qwp.len(),
412                    self.max_buf_size
413                ));
414            }
415            if transactional {
416                return Err(error::fmt!(
417                    InvalidApiCall,
418                    "Transactional flushes are not supported for QWP/UDP."
419                ));
420            }
421            return flush_qwp_udp(state, qwp);
422        }
423
424        #[cfg(feature = "sync-sender-qwp-ws")]
425        if matches!(
426            &self.handler,
427            SyncProtocolHandler::SyncQwpWs(_) | SyncProtocolHandler::ManualQwpWs(_)
428        ) {
429            return self.flush_qwp_ws_buffer(buf, transactional).map(|_| ());
430        }
431
432        if !self.connected {
433            return Err(error::fmt!(
434                SocketError,
435                "Could not flush buffer: not connected to database."
436            ));
437        }
438        let ilp = buf.as_ilp().ok_or_else(|| {
439            error::fmt!(
440                InvalidApiCall,
441                "ILP sender requires an ILP buffer. QWP buffers must be flushed with a QWP/UDP sender."
442            )
443        })?;
444        ilp.check_can_flush()?;
445
446        let bytes = ilp.as_bytes();
447        if bytes.is_empty() {
448            return Ok(());
449        }
450
451        if ilp.len() > self.max_buf_size {
452            return Err(error::fmt!(
453                InvalidApiCall,
454                "Could not flush buffer: Buffer size of {} exceeds maximum configured allowed size of {} bytes.",
455                ilp.len(),
456                self.max_buf_size
457            ));
458        }
459
460        self.check_protocol_version(ilp.protocol_version())?;
461        match self.handler {
462            #[cfg(feature = "sync-sender-tcp")]
463            SyncProtocolHandler::SyncTcp(ref mut conn) => {
464                if transactional {
465                    return Err(error::fmt!(
466                        InvalidApiCall,
467                        "Transactional flushes are not supported for ILP over TCP."
468                    ));
469                }
470                conn.write_all(bytes).map_err(|io_err| {
471                    self.connected = false;
472                    map_io_to_socket_err("Could not flush buffer: ", io_err)
473                })?;
474                conn.flush().map_err(|io_err| {
475                    self.connected = false;
476                    map_io_to_socket_err("Could not flush to network: ", io_err)
477                })?;
478                Ok(())
479            }
480            #[cfg(feature = "sync-sender-http")]
481            SyncProtocolHandler::SyncHttp(ref state) => {
482                if transactional && !ilp.transactional() {
483                    return Err(error::fmt!(
484                        InvalidApiCall,
485                        "Buffer contains lines for multiple tables. \
486                        Transactional flushes are only supported for buffers containing lines for a single table."
487                    ));
488                }
489                let request_min_throughput = *state.config.request_min_throughput;
490                let extra_time = if request_min_throughput > 0 {
491                    (bytes.len() as f64) / (request_min_throughput as f64)
492                } else {
493                    0.0f64
494                };
495
496                match http_send_with_retries(
497                    state,
498                    bytes,
499                    *state.config.request_timeout + std::time::Duration::from_secs_f64(extra_time),
500                    *state.config.retry_timeout,
501                    *state.config.retry_max_backoff,
502                ) {
503                    Ok(res) => {
504                        if res.status().is_client_error() || res.status().is_server_error() {
505                            Err(parse_http_error(res.status().as_u16(), res))
506                        } else {
507                            res.into_body();
508                            Ok(())
509                        }
510                    }
511                    Err(err) => Err(crate::error::Error::from_ureq_error(err, &state.url)),
512                }
513            }
514            #[cfg(feature = "sync-sender-qwp-udp")]
515            SyncProtocolHandler::SyncQwpUdp(_) => Err(error::fmt!(
516                InvalidApiCall,
517                "internal error: QWP/UDP handler in ILP flush path"
518            )),
519            #[cfg(feature = "sync-sender-qwp-ws")]
520            SyncProtocolHandler::SyncQwpWs(_) => Err(error::fmt!(
521                InvalidApiCall,
522                "internal error: QWP/WebSocket handler in ILP flush path"
523            )),
524            #[cfg(feature = "sync-sender-qwp-ws")]
525            SyncProtocolHandler::ManualQwpWs(_) => Err(error::fmt!(
526                InvalidApiCall,
527                "internal error: manual QWP/WebSocket handler in ILP flush path"
528            )),
529        }
530    }
531
532    /// Send the batch of rows in the buffer to the QuestDB server, and, if the
533    /// `transactional` parameter is true, ensure the flush will be transactional.
534    ///
535    /// A flush is transactional iff all the rows belong to the same table. This allows
536    /// QuestDB to treat the flush as a single database transaction, because it doesn't
537    /// support transactions spanning multiple tables. Additionally, only ILP-over-HTTP
538    /// supports transactional flushes; QWP/UDP is a best-effort datagram transport and
539    /// has no flush-level atomicity guarantee.
540    ///
541    /// If the flush wouldn't be transactional, this function returns an error and
542    /// doesn't flush any data.
543    ///
544    /// The function sends an HTTP request and waits for the response. If the server
545    /// responds with an error, it returns a descriptive error. In the case of a network
546    /// error, it retries until it has exhausted the retry time budget.
547    ///
548    /// All the data stays in the buffer. Clear the buffer before starting a new batch.
549    #[cfg(feature = "sync-sender-http")]
550    pub fn flush_and_keep_with_flags(&mut self, buf: &Buffer, transactional: bool) -> Result<()> {
551        self.flush_impl(buf, transactional)
552    }
553
554    /// Send the given buffer of rows to the QuestDB server.
555    ///
556    /// All the data stays in the buffer. Clear the buffer before starting a new batch.
557    ///
558    /// To send and clear in one step, call [Sender::flush] instead.
559    pub fn flush_and_keep(&mut self, buf: &Buffer) -> Result<()> {
560        self.flush_impl(buf, false)
561    }
562
563    /// Send the given buffer of rows to the QuestDB server, clearing the buffer.
564    ///
565    /// After this function returns, the buffer is empty and ready for the next batch.
566    /// If you want to preserve the buffer contents, call [Sender::flush_and_keep]. If
567    /// you want to ensure the flush is transactional, call
568    /// [Sender::flush_and_keep_with_flags].
569    ///
570    /// With ILP-over-HTTP, this function sends an HTTP request and waits for the
571    /// response. If the server responds with an error, it returns a descriptive error.
572    /// In the case of a network error, it retries until it has exhausted the retry time
573    /// budget.
574    ///
575    /// With ILP-over-TCP, the function blocks only until the buffer is flushed to the
576    /// underlying OS-level network socket, without waiting to actually send it to the
577    /// server. In the case of an error, the server will quietly disconnect: consult the
578    /// server logs for error messages.
579    ///
580    /// With QWP-over-UDP, the function sends one or more UDP datagrams and returns
581    /// local socket errors only. A successful return does not guarantee delivery, and
582    /// when a flush spans multiple datagrams there is no all-or-nothing guarantee for
583    /// the logical batch.
584    ///
585    /// With QWP-over-WebSocket, the function publishes the rows into local
586    /// memory or Store-and-Forward storage and returns without waiting for the
587    /// submitted frame's server ACK. It may still wait for local capacity. In
588    /// the default background progress mode, a sender-owned runner sends,
589    /// receives ACKs, reconnects, and replays as needed. In manual progress
590    /// mode, the caller must use `Sender::drive_once` or
591    /// `Sender::wait` to advance WebSocket progress. Server or
592    /// transport failures observed later are reported by subsequent sender
593    /// calls.
594    ///
595    /// HTTP should be the first choice, but use TCP if you need to continuously send
596    /// data to the server at a high rate.
597    ///
598    /// To improve the HTTP performance, send larger buffers (with more rows), and
599    /// consider parallelizing writes using multiple senders from multiple threads.
600    pub fn flush(&mut self, buf: &mut Buffer) -> crate::Result<()> {
601        self.flush_impl(buf, false)?;
602        buf.clear();
603        Ok(())
604    }
605
606    /// Publish the QWP/WebSocket buffer and return the highest published frame
607    /// sequence number.
608    ///
609    /// This is QWP/WebSocket-specific. It has the same local-publication
610    /// semantics as [`Sender::flush`]: it returns after the frame is accepted
611    /// by the local replay queue, before the server necessarily ACKs it. Empty
612    /// buffers return `Ok(None)`.
613    ///
614    /// Use this when you need non-blocking/pipelined progress tracking on this
615    /// sender stream: keep the returned FSN and compare it with
616    /// [`Self::acked_fsn`]. Use [`Self::wait`] instead when you only need a
617    /// blocking barrier for everything published so far.
618    #[cfg(feature = "sync-sender-qwp-ws")]
619    pub fn flush_and_get_fsn(&mut self, buf: &mut Buffer) -> Result<Option<u64>> {
620        let fsn = self.flush_and_keep_and_get_fsn(buf)?;
621        buf.clear();
622        Ok(fsn)
623    }
624
625    /// Publish the QWP/WebSocket buffer without clearing it and return the
626    /// highest published frame sequence number.
627    ///
628    /// The returned FSN has the same local-publication semantics as
629    /// [`Self::flush_and_get_fsn`].
630    #[cfg(feature = "sync-sender-qwp-ws")]
631    pub fn flush_and_keep_and_get_fsn(&mut self, buf: &Buffer) -> Result<Option<u64>> {
632        self.flush_qwp_ws_buffer(buf, false)
633    }
634
635    /// Return the highest frame sequence number published locally by this
636    /// QWP/WebSocket sender, or `None` if no frame has been published.
637    ///
638    /// This is a sender-stream watermark, not a process-global receipt.
639    #[cfg(feature = "sync-sender-qwp-ws")]
640    pub fn published_fsn(&self) -> Result<Option<u64>> {
641        match &self.handler {
642            SyncProtocolHandler::SyncQwpWs(state) => qwp_ws_published_fsn_background(state),
643            SyncProtocolHandler::ManualQwpWs(state) => qwp_ws_published_fsn_manual(state),
644            _ => Err(error::fmt!(
645                InvalidApiCall,
646                "published_fsn is only supported for QWP/WebSocket senders."
647            )),
648        }
649    }
650
651    /// Return the highest frame sequence number completed by server ACK or
652    /// server-side reject-and-continue, or `None` if no frame has completed.
653    /// In QWP/WebSocket durable ACK mode, ordinary OK frames do not advance
654    /// this watermark; it advances once durable ACKs cover the frame.
655    ///
656    /// After [`Self::flush_and_get_fsn`] returns `Some(fsn)`, that publication
657    /// boundary has completed once this method returns a value greater than or
658    /// equal to `fsn`. Use [`Self::wait`] when you need an explicit
659    /// [`AckLevel::Ok`] or [`AckLevel::Durable`] barrier.
660    #[cfg(feature = "sync-sender-qwp-ws")]
661    pub fn acked_fsn(&self) -> Result<Option<u64>> {
662        match &self.handler {
663            SyncProtocolHandler::SyncQwpWs(state) => qwp_ws_acked_fsn_background(state),
664            SyncProtocolHandler::ManualQwpWs(state) => qwp_ws_acked_fsn_manual(state),
665            _ => Err(error::fmt!(
666                InvalidApiCall,
667                "acked_fsn is only supported for QWP/WebSocket senders."
668            )),
669        }
670    }
671
672    /// Wait until every QWP/WebSocket frame published so far on this sender
673    /// reaches `ack_level`, or until the wait makes no progress for `timeout`.
674    ///
675    /// This is the row-major counterpart to the column-major
676    /// [`crate::BorrowedSender::wait`]: it takes the cumulative publication
677    /// boundary ([`Self::published_fsn`]) and blocks until the requested
678    /// completion watermark covers it.
679    ///
680    /// * [`AckLevel::Ok`] waits for the server to accept every published
681    ///   frame.
682    /// * [`AckLevel::Durable`] waits for durable-ACK coverage. It requires
683    ///   QuestDB Enterprise and a sender opened with
684    ///   `request_durable_ack=on`; otherwise the call is rejected before
685    ///   checking whether any frame has been published.
686    ///
687    /// `timeout` is a **no-progress** deadline: it fires only if the ack
688    /// watermark fails to advance for that long, so a steadily-progressing
689    /// large batch keeps waiting. `Duration::ZERO` waits indefinitely. On
690    /// expiry it returns an
691    /// [`ErrorCode::FailoverRetry`](crate::ErrorCode::FailoverRetry)
692    /// error and the published frames are retained for replay.
693    ///
694    /// A terminal server rejection of a frame in the pending range, or a
695    /// terminal transport/protocol failure, is returned as an error. Retriable
696    /// server rejections reconnect and replay until the frame is acknowledged or
697    /// the sender is stopped. When nothing has been published yet, a valid wait
698    /// returns immediately. QWP/WebSocket only; other
699    /// protocols return `InvalidApiCall`. In manual progress mode this also
700    /// drives WebSocket progress while waiting.
701    #[cfg(feature = "sync-sender-qwp-ws")]
702    pub fn wait(&mut self, ack_level: AckLevel, timeout: Duration) -> Result<()> {
703        let result = self.wait_inner(ack_level, timeout);
704        let drain_result = self.drain_qwp_ws_error_notifications();
705        result.and(drain_result)
706    }
707
708    #[cfg(feature = "sync-sender-qwp-ws")]
709    fn wait_inner(&mut self, ack_level: AckLevel, timeout: Duration) -> Result<()> {
710        if !matches!(
711            &self.handler,
712            SyncProtocolHandler::SyncQwpWs(_) | SyncProtocolHandler::ManualQwpWs(_)
713        ) {
714            return Err(error::fmt!(
715                InvalidApiCall,
716                "wait is only supported for QWP/WebSocket senders."
717            ));
718        }
719
720        if ack_level == AckLevel::Durable {
721            let request_durable_ack = match &self.handler {
722                SyncProtocolHandler::SyncQwpWs(state) => state.request_durable_ack,
723                SyncProtocolHandler::ManualQwpWs(state) => state.request_durable_ack,
724                _ => unreachable!("QWP/WebSocket handler was checked above"),
725            };
726            if !request_durable_ack {
727                return Err(error::fmt!(
728                    InvalidApiCall,
729                    "AckLevel::Durable requires the pool to be opened with \
730                     `request_durable_ack=on` in the connect string."
731                ));
732            }
733        }
734
735        let Some(boundary) = self.published_fsn()? else {
736            return Ok(());
737        };
738
739        // No-progress deadline: reset whenever the completion watermark
740        // advances, so it only fires when the peer stays alive yet silent.
741        let mut deadline_anchor = Instant::now();
742        let mut last_completed: Option<u64> = None;
743
744        loop {
745            let completed = self.qwp_ws_completed_fsn(ack_level)?;
746            if completed.is_some_and(|fsn| fsn >= boundary) {
747                return Ok(());
748            }
749            if completed != last_completed {
750                last_completed = completed;
751                deadline_anchor = Instant::now();
752            }
753            if !timeout.is_zero() && deadline_anchor.elapsed() >= timeout {
754                return Err(qwp_ws_wait_timeout(ack_level, timeout, boundary, completed));
755            }
756
757            match &mut self.handler {
758                SyncProtocolHandler::ManualQwpWs(state) => {
759                    if !qwp_ws_drive_once(state)? {
760                        qwp_ws_sleep_until(None);
761                    }
762                }
763                SyncProtocolHandler::SyncQwpWs(_) => qwp_ws_sleep_until(None),
764                _ => unreachable!("QWP/WebSocket handler was checked above"),
765            }
766        }
767    }
768
769    /// Completion watermark for `ack_level` across both QWP/WebSocket progress
770    /// modes. `Ok` tracks server acceptance; `Durable` tracks durable-ACK
771    /// coverage. Terminal failures surface here as an `Err`.
772    #[cfg(feature = "sync-sender-qwp-ws")]
773    fn qwp_ws_completed_fsn(&self, ack_level: AckLevel) -> Result<Option<u64>> {
774        match (&self.handler, ack_level) {
775            (SyncProtocolHandler::SyncQwpWs(state), AckLevel::Ok) => {
776                qwp_ws_ok_fsn_background(state)
777            }
778            (SyncProtocolHandler::SyncQwpWs(state), AckLevel::Durable) => {
779                qwp_ws_acked_fsn_background(state)
780            }
781            (SyncProtocolHandler::ManualQwpWs(state), AckLevel::Ok) => qwp_ws_ok_fsn_manual(state),
782            (SyncProtocolHandler::ManualQwpWs(state), AckLevel::Durable) => {
783                qwp_ws_acked_fsn_manual(state)
784            }
785            _ => Err(error::fmt!(
786                InvalidApiCall,
787                "wait is only supported for QWP/WebSocket senders."
788            )),
789        }
790    }
791
792    /// Poll the next structured QWP/WebSocket server error observed by this
793    /// sender.
794    ///
795    /// This reports QWP server non-OK responses and WebSocket protocol
796    /// violations. It remains usable after the sender has halted so
797    /// callers can inspect the error that made it terminal.
798    #[cfg(feature = "sync-sender-qwp-ws")]
799    pub fn poll_qwp_ws_error(&mut self) -> Result<Option<QwpWsSenderError>> {
800        match &mut self.handler {
801            SyncProtocolHandler::SyncQwpWs(state) => qwp_ws_poll_sender_error_background(state),
802            SyncProtocolHandler::ManualQwpWs(state) => qwp_ws_poll_sender_error_manual(state),
803            _ => Err(error::fmt!(
804                InvalidApiCall,
805                "poll_qwp_ws_error is only supported for QWP/WebSocket senders."
806            )),
807        }
808    }
809
810    /// Return the structured QWP/WebSocket diagnostic that halted this sender,
811    /// if terminalization was caused by a QWP/WebSocket server or protocol
812    /// error.
813    ///
814    /// Unlike [`Sender::poll_qwp_ws_error`], this does not consume the diagnostic.
815    #[cfg(feature = "sync-sender-qwp-ws")]
816    #[doc(hidden)]
817    pub fn qwp_ws_terminal_error(&self) -> Result<Option<QwpWsSenderError>> {
818        match &self.handler {
819            SyncProtocolHandler::SyncQwpWs(state) => qwp_ws_terminal_sender_error_background(state),
820            SyncProtocolHandler::ManualQwpWs(state) => qwp_ws_terminal_sender_error_manual(state),
821            _ => Err(error::fmt!(
822                InvalidApiCall,
823                "qwp_ws_terminal_error is only supported for QWP/WebSocket senders."
824            )),
825        }
826    }
827
828    /// Return how many QWP/WebSocket structured diagnostics were dropped
829    /// because the sender's unified bounded diagnostic log was full.
830    ///
831    /// The same log feeds [`Sender::poll_qwp_ws_error`] and
832    /// `QwpWsErrorHandler` notification delivery through independent cursors.
833    /// A diagnostic is retained until both cursors have consumed it, so a
834    /// lagging cursor can cause later diagnostics to overwrite unread entries
835    /// and increment this count.
836    #[cfg(feature = "sync-sender-qwp-ws")]
837    pub fn qwp_ws_errors_dropped(&self) -> Result<u64> {
838        match &self.handler {
839            SyncProtocolHandler::SyncQwpWs(state) => qwp_ws_sender_errors_dropped_background(state),
840            SyncProtocolHandler::ManualQwpWs(state) => qwp_ws_sender_errors_dropped_manual(state),
841            _ => Err(error::fmt!(
842                InvalidApiCall,
843                "qwp_ws_errors_dropped is only supported for QWP/WebSocket senders."
844            )),
845        }
846    }
847
848    /// Snapshot the QWP/WebSocket sender's lifetime totals.
849    ///
850    /// Mirrors the `getTotal*` counters on Java's `QwpWebSocketSender` so the
851    /// QuestDB Enterprise e2e harness (questdb-ent/e2e) can read identical
852    /// signals across language bindings. See [`QwpWsTotals`] for the field
853    /// list. Returns `InvalidApiCall` for non-QWP/WebSocket senders.
854    #[cfg(feature = "sync-sender-qwp-ws")]
855    pub fn qwp_ws_totals(&self) -> Result<QwpWsTotals> {
856        let counters = match &self.handler {
857            SyncProtocolHandler::SyncQwpWs(state) => qwp_ws_counters_background(state)?,
858            SyncProtocolHandler::ManualQwpWs(state) => qwp_ws_counters_manual(state)?,
859            _ => {
860                return Err(error::fmt!(
861                    InvalidApiCall,
862                    "qwp_ws_totals is only supported for QWP/WebSocket senders."
863                ));
864            }
865        };
866        Ok(counters.into())
867    }
868
869    /// Drive one QWP/WebSocket progress step when the sender was built with
870    /// [`QwpWsProgress::Manual`].
871    ///
872    /// One call performs, in order:
873    /// - send at most one queued frame;
874    /// - drain all ready response frames from the transport (acks, durable
875    ///   acks, rejects), applying their effects on local store state;
876    /// - perform at most one bounded storage-maintenance step (provision a
877    ///   missing hot spare or trim one fully-acked sealed segment) when
878    ///   Store-and-Forward is configured;
879    /// - send a durable-ACK keepalive only if nothing above produced
880    ///   progress and one is due.
881    ///
882    /// Returns `Ok(true)` if any of those steps produced progress and
883    /// `Ok(false)` when the call was idle. Manual schedulers should keep
884    /// calling `drive_once` until it returns `false` before parking, since the
885    /// receive drain and storage maintenance are paced one unit per call:
886    /// hot-spare provisioning and segment trim each take their own call, so a
887    /// large ACK can free segment-cap headroom over several `drive_once`
888    /// turns.
889    #[cfg(feature = "sync-sender-qwp-ws")]
890    pub fn drive_once(&mut self) -> Result<bool> {
891        let result = match &mut self.handler {
892            SyncProtocolHandler::ManualQwpWs(state) => qwp_ws_drive_once(state),
893            SyncProtocolHandler::SyncQwpWs(_) => {
894                return Err(error::fmt!(
895                    InvalidApiCall,
896                    "drive_once is only supported when qwp_ws_progress is manual."
897                ));
898            }
899            _ => {
900                return Err(error::fmt!(
901                    InvalidApiCall,
902                    "drive_once is only supported for QWP/WebSocket senders."
903                ));
904            }
905        };
906        let drain_result = self.drain_qwp_ws_error_notifications();
907        let progressed = result?;
908        drain_result?;
909        Ok(progressed)
910    }
911
912    /// Stop accepting new QWP/WebSocket publications and wait for all already
913    /// published frames to complete.
914    ///
915    /// The wait is bounded by the QWP/WebSocket `close_flush_timeout_millis`
916    /// setting. Its default is 5000 ms, matching the Java sender. Values less
917    /// than or equal to zero skip the wait.
918    #[cfg(feature = "sync-sender-qwp-ws")]
919    pub fn close_drain(&mut self) -> Result<()> {
920        let result = match &mut self.handler {
921            SyncProtocolHandler::SyncQwpWs(state) => qwp_ws_close_drain_background(state),
922            SyncProtocolHandler::ManualQwpWs(state) => qwp_ws_close_drain_manual(state),
923            _ => Err(error::fmt!(
924                InvalidApiCall,
925                "close_drain is only supported for QWP/WebSocket senders."
926            )),
927        };
928        let drain_result = if matches!(
929            &self.handler,
930            SyncProtocolHandler::SyncQwpWs(_) | SyncProtocolHandler::ManualQwpWs(_)
931        ) {
932            self.drain_qwp_ws_error_notifications()
933        } else {
934            Ok(())
935        };
936        if result
937            .as_ref()
938            .is_err_and(|err| matches!(err.code(), crate::ErrorCode::SocketError))
939        {
940            self.connected = false;
941        }
942        result.and(drain_result)
943    }
944
945    /// Tell whether the sender is no longer usable and must be dropped.
946    ///
947    /// Returns `true` after an unrecoverable failure. For ILP-over-TCP this
948    /// is any socket error. For QWP/WebSocket this also covers a server
949    /// rejection or protocol violation that latches the publication
950    /// lifecycle to its terminal state. ILP-over-HTTP and QWP/UDP never
951    /// transition into a permanently-unusable state and always return
952    /// `false`.
953    ///
954    /// In QWP/WebSocket manual progress mode the answer only refreshes when
955    /// the user drives the sender (`drive_once` / `flush`), since no
956    /// background thread is observing the transport.
957    #[must_use]
958    pub fn must_close(&self) -> bool {
959        if !self.connected {
960            return true;
961        }
962        #[cfg(feature = "sync-sender-qwp-ws")]
963        match &self.handler {
964            SyncProtocolHandler::SyncQwpWs(state) => return qwp_ws_is_terminal_background(state),
965            SyncProtocolHandler::ManualQwpWs(state) => return qwp_ws_is_terminal_manual(state),
966            _ => {}
967        }
968        false
969    }
970
971    /// Test-only non-blocking view of whether a background QWP/WebSocket
972    /// store-and-forward sender has no undelivered published frames.
973    /// Non-QWP/WebSocket handlers and terminal background handlers report
974    /// `true`. Retained for the standalone sender regression test after the
975    /// pooled row reaper that consumed it was removed.
976    #[cfg(all(test, feature = "sync-sender-qwp-ws"))]
977    pub(crate) fn sfa_fully_delivered(&self, durable: bool) -> bool {
978        let SyncProtocolHandler::SyncQwpWs(state) = &self.handler else {
979            return true;
980        };
981        if qwp_ws_is_terminal_background(state) {
982            return true;
983        }
984        let Ok(Some(published)) = qwp_ws_published_fsn_background(state) else {
985            return true;
986        };
987        let watermark = if durable {
988            qwp_ws_acked_fsn_background(state)
989        } else {
990            qwp_ws_ok_fsn_background(state)
991        };
992        matches!(watermark, Ok(Some(w)) if w >= published)
993    }
994
995    /// Returns the sender's configured transport protocol.
996    pub fn protocol(&self) -> Protocol {
997        self.protocol
998    }
999
1000    /// Returns the sender's protocol version.
1001    ///
1002    /// The returned value may be explicitly configured, auto-detected, or a
1003    /// transport-defined default. Interpret it together with [`Sender::protocol`]
1004    /// and [`ProtocolVersion`]. For QWP/UDP this reports the QWP datagram
1005    /// version, currently represented as [`ProtocolVersion::V1`]; it is not an
1006    /// ILP feature version.
1007    pub fn protocol_version(&self) -> ProtocolVersion {
1008        self.protocol_version
1009    }
1010
1011    /// Return the sender's maxinum name length of any column or table name.
1012    /// This is either set explicitly when constructing the sender,
1013    /// or the default value of 127.
1014    /// When unset and using protocol version 2 over HTTP, the value is read
1015    /// from the server from the `cairo.max.file.name.length` setting in
1016    /// `server.conf` which defaults to 127.
1017    pub fn max_name_len(&self) -> usize {
1018        self.max_name_len
1019    }
1020
1021    #[inline(always)]
1022    fn check_protocol_version(&self, version: ProtocolVersion) -> Result<()> {
1023        if self.protocol_version != version {
1024            return Err(error::fmt!(
1025                ProtocolVersionError,
1026                "Attempting to send with protocol version {} \
1027                but the sender is configured to use protocol version {}",
1028                version,
1029                self.protocol_version
1030            ));
1031        }
1032        Ok(())
1033    }
1034}
1035
1036#[cfg(feature = "sync-sender-qwp-ws")]
1037fn qwp_ws_deadline_expired(deadline: Option<Instant>) -> bool {
1038    deadline.is_some_and(|deadline| Instant::now() >= deadline)
1039}
1040
1041#[cfg(feature = "sync-sender-qwp-ws")]
1042fn qwp_ws_sleep_until(deadline: Option<Instant>) {
1043    const PARK: Duration = Duration::from_micros(50);
1044    if qwp_ws_deadline_expired(deadline) {
1045        return;
1046    }
1047    let sleep_for = deadline
1048        .map(|deadline| deadline.saturating_duration_since(Instant::now()).min(PARK))
1049        .unwrap_or(PARK);
1050    if !sleep_for.is_zero() {
1051        std::thread::sleep(sleep_for);
1052    }
1053}
1054
1055/// Error for a [`Sender::wait`] that made no ack progress within its
1056/// no-progress `timeout`. Classified [`ErrorCode::FailoverRetry`]: the
1057/// published frames are retained and the background runner keeps delivering
1058/// them, so recover by retrying `wait()` until it returns `Ok` — not by
1059/// re-flushing, which would duplicate the rows. Mirrors the column-major
1060/// store-and-forward wait.
1061#[cfg(feature = "sync-sender-qwp-ws")]
1062fn qwp_ws_wait_timeout(
1063    ack_level: AckLevel,
1064    timeout: Duration,
1065    boundary: u64,
1066    completed: Option<u64>,
1067) -> crate::Error {
1068    let level = match ack_level {
1069        AckLevel::Ok => "ok",
1070        AckLevel::Durable => "durable",
1071    };
1072    let progress = match completed {
1073        Some(fsn) => format!("reached FSN {fsn}"),
1074        None => "reached no frame".to_string(),
1075    };
1076    error::Error::new(
1077        error::ErrorCode::FailoverRetry,
1078        format!(
1079            "QWP/WebSocket wait({level}) timed out after {timeout:?} with no ack \
1080             progress (target FSN {boundary}, {progress}); the connection is alive \
1081             but the server is not advancing the watermark. The published frames \
1082             remain queued and the background runner keeps delivering them: retry \
1083             wait() to keep awaiting the ack, or close the pool to drain. Do not \
1084             re-flush the same data, which is already accepted and would be \
1085             delivered twice."
1086        ),
1087    )
1088}