Skip to main content

influxdb3_client/
write.rs

1use std::collections::HashMap;
2
3use crate::{error::Error, point::Point, precision::Precision};
4
5/// Options controlling a single write operation.
6///
7/// Defaults live in [`crate::ClientConfig::write_options`]; individual writes
8/// override values via the [`crate::Client::write`] builder.
9#[derive(Debug, Clone)]
10pub struct WriteOptions {
11    /// Timestamp precision for this write.  Defaults to `Nanosecond`.
12    pub precision: Precision,
13
14    /// Tags merged into every point before serialisation.
15    /// Point-level tags take precedence on collision.
16    pub default_tags: HashMap<String, String>,
17
18    /// If `Some(n)`, compress the body with gzip when it exceeds `n` bytes.
19    /// `Some(0)` always compresses; `None` never compresses. Defaults to
20    /// `Some(1024)`.
21    ///
22    /// Compression trades CPU for bandwidth. The default suits remote/cloud
23    /// targets where bandwidth dominates. For high-throughput ingest over a
24    /// fast LAN (flight-test, IIoT), gzip CPU can become the bottleneck. Set
25    /// `gzip_threshold(None)` to disable it, or raise the threshold so only
26    /// large batches are compressed.
27    pub gzip_threshold: Option<usize>,
28
29    /// When `true`, skip WAL synchronisation (faster, lower durability).
30    pub no_sync: bool,
31
32    /// When `true`, a batch is accepted even if some lines are invalid.
33    pub accept_partial: bool,
34
35    /// When `true`, use the V2 (`/api/v2/write`) endpoint instead of V3.
36    pub use_v2_api: bool,
37
38    /// Optional tag ordering for deterministic line-protocol output.
39    pub tag_order: Vec<String>,
40
41    /// Maximum number of points per HTTP request when calling `write`.
42    /// Larger inputs are streamed as multiple sequential or pipelined requests.
43    /// Defaults to `5_000`.
44    ///
45    /// This is a point count, not a byte size. The effective ceiling is the
46    /// server's maximum request size (configurable on InfluxDB, 10 MB by
47    /// default); if you raise `batch_size` into a `413`, raise that limit too.
48    pub batch_size: usize,
49
50    /// Maximum number of concurrent in-flight HTTP requests when writing
51    /// multiple batches.  Defaults to `4`.  Set to `1` for strict ordering.
52    pub max_inflight: usize,
53}
54
55/// Default maximum number of points per write request.
56pub const DEFAULT_BATCH_SIZE: usize = 5_000;
57
58/// Default maximum number of concurrent in-flight HTTP write requests.
59pub const DEFAULT_MAX_INFLIGHT: usize = 4;
60
61impl Default for WriteOptions {
62    fn default() -> Self {
63        WriteOptions {
64            precision: Precision::Nanosecond,
65            default_tags: HashMap::new(),
66            gzip_threshold: Some(1024),
67            no_sync: false,
68            accept_partial: true,
69            use_v2_api: true,
70            tag_order: Vec::new(),
71            batch_size: DEFAULT_BATCH_SIZE,
72            max_inflight: DEFAULT_MAX_INFLIGHT,
73        }
74    }
75}
76
77impl WriteOptions {
78    pub(crate) fn validate(&self) -> Result<(), Error> {
79        if self.use_v2_api && self.no_sync {
80            return Err(Error::Config(
81                "invalid write options: no_sync requires use_v2_api=false".into(),
82            ));
83        }
84        Ok(())
85    }
86}
87
88/// A type that can be lazily serialised to InfluxDB line protocol for writing.
89///
90/// Pass anything that implements this trait to [`crate::Client::write`].
91///
92/// | Type | Use case |
93/// |---|---|
94/// | `&str` / `String` | Pre-formatted line protocol (low-level escape hatch) |
95/// | `Vec<Point>` | Point builder API (pass ownership; clone a slice with `.to_vec()` if you must keep it) |
96/// | [`crate::write_dataframe::DataFrameWrite`] | polars DataFrame (`polars` feature) |
97///
98/// Implementations return an iterator that yields **one batch per HTTP
99/// request**. The iterator is consumed lazily, so only one batch buffer lives
100/// in memory at a time even for million-point writes.
101pub trait WriteInput {
102    /// Lazily produce line-protocol batches, one per HTTP request.
103    ///
104    /// Implementations should respect `opts.batch_size`.  Errors per batch are
105    /// returned in the iterator so partially-valid inputs can still send what
106    /// they can.
107    fn into_lp_batches(
108        self,
109        opts: &WriteOptions,
110    ) -> Box<dyn Iterator<Item = crate::Result<Vec<u8>>> + Send>;
111}
112
113impl WriteInput for &str {
114    fn into_lp_batches(
115        self,
116        _opts: &WriteOptions,
117    ) -> Box<dyn Iterator<Item = crate::Result<Vec<u8>>> + Send> {
118        if self.is_empty() {
119            Box::new(std::iter::empty())
120        } else {
121            Box::new(std::iter::once(Ok(self.as_bytes().to_vec())))
122        }
123    }
124}
125
126impl WriteInput for String {
127    fn into_lp_batches(
128        self,
129        _opts: &WriteOptions,
130    ) -> Box<dyn Iterator<Item = crate::Result<Vec<u8>>> + Send> {
131        if self.is_empty() {
132            Box::new(std::iter::empty())
133        } else {
134            Box::new(std::iter::once(Ok(self.into_bytes())))
135        }
136    }
137}
138
139/// Lazy iterator that serialises chunks of points into LP buffers on demand.
140pub(crate) struct PointBatchIter {
141    points: Vec<Point>,
142    idx: usize,
143    batch_size: usize,
144    precision: Precision,
145    default_tags: HashMap<String, String>,
146    tag_order: Vec<String>,
147}
148
149impl Iterator for PointBatchIter {
150    type Item = crate::Result<Vec<u8>>;
151
152    fn next(&mut self) -> Option<Self::Item> {
153        if self.idx >= self.points.len() {
154            return None;
155        }
156        let end = (self.idx + self.batch_size).min(self.points.len());
157        // Pre-size at roughly 64 bytes per point.
158        let mut buf = Vec::with_capacity((end - self.idx) * 64);
159        let tag_order = if self.tag_order.is_empty() {
160            None
161        } else {
162            Some(self.tag_order.as_slice())
163        };
164        // One scratch buffer reused for every point in the batch.
165        let mut key_scratch = Vec::new();
166        for point in &self.points[self.idx..end] {
167            if let Err(e) = point.write_line_protocol(
168                &mut buf,
169                self.precision,
170                &self.default_tags,
171                tag_order,
172                &mut key_scratch,
173            ) {
174                self.idx = self.points.len(); // stop iteration after error
175                return Some(Err(e));
176            }
177            buf.push(b'\n');
178        }
179        // Drop the trailing newline.
180        if buf.last() == Some(&b'\n') {
181            buf.pop();
182        }
183        self.idx = end;
184        Some(Ok(buf))
185    }
186}
187
188impl WriteInput for Vec<Point> {
189    fn into_lp_batches(
190        self,
191        opts: &WriteOptions,
192    ) -> Box<dyn Iterator<Item = crate::Result<Vec<u8>>> + Send> {
193        Box::new(PointBatchIter {
194            points: self,
195            idx: 0,
196            batch_size: opts.batch_size.max(1),
197            precision: opts.precision,
198            default_tags: opts.default_tags.clone(),
199            tag_order: opts.tag_order.clone(),
200        })
201    }
202}