1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
//! Segments: what a stream's rows become when they are sealed, and the
//! boundary across which dendro does not know what a row means.
use Arc;
use Schema;
use RecordBatch;
use ArrowWriter;
use Compression;
use WriterProperties;
use crateWalRow;
use crate;
/// One sealed segment: parquet bytes plus the catalog facts about what is in
/// them.
///
/// **Every field describes the SEGMENT, not the rows it was given.** An encoder
/// is free to drop rows it cannot encode - one whose rows reference a schema
/// anchor that retention has evicted has no other option - and the container
/// has to catalog what was actually written or the catalog and the bytes
/// disagree.
///
/// `last_ts` matters most, because it is what the WAL prune and the read
/// watermark are computed from. Taking it from the INPUT instead, as this
/// crate did until it was measured, deletes rows the segment does not contain:
/// they are gone from the WAL, absent from the bytes, and shadowed by a
/// watermark that claims coverage up to a timestamp nothing holds. Reporting it
/// here means a dropped trailing row stays live and is sealed by the
/// next batch.
///
/// The writer validates these against the rows it supplied - see
/// `writer::seal_batch`. The check that matters is CONTIGUITY: the writer
/// counts how many of the rows it handed over fall inside `[first_ts,
/// last_ts]`, and requires that to equal `rows`. A hole anywhere inside the
/// claimed span would be rows that end up in no segment and no WAL, because
/// the prune deletes everything up to `last_ts`.
///
/// So an encoder may drop a LEADING or a TRAILING run - both narrow the span
/// without holing it, and a trailing drop leaves those rows live for
/// the next batch. It may not drop from the middle, and it may not claim rows
/// its span does not hold.
/// Turns a stream's WAL rows into one parquet segment.
///
/// **This is the schema boundary.** dendro stores a WAL row as an opaque
/// BLOB keyed by `(source, stream, ts)`; what those bytes mean, and what
/// columns they become, is entirely the caller's. Both the writer thread (when
/// it seals) and any independent reader (materializing a live tail out of an
/// archive another process is appending to) call this, so an implementation
/// must work from the rows alone: a reader has none of the writer's in-memory
/// state, so anything an encode needs must travel in the rows.
///
/// `None` means that the rows produce no segment.
///
/// **A column must mean one thing for the life of a stream.** Its name, its
/// type and its field metadata are the column's identity, and the archive
/// treats two segments whose columns share all three as holding one series:
/// compaction concatenates them, and a reader reads them end to end. A fact
/// that changes over time, such as which task a slot currently stands for,
/// must not be carried in field metadata. Rows that span such a change fuse
/// two series into one column, the segment that results is valid parquet
/// with nothing to show it happened, and nothing can separate them
/// afterward. Keep such facts in the caller's time-keyed store instead
/// ([`CallerRow`](crate::archive::CallerRow)), keyed by the time they
/// changed, and keep the column static.
///
/// **Called with an empty slice**, on every read of a stream with nothing
/// unsealed — which is every read of a finalized archive. An implementation
/// that indexes `rows[0]` without checking panics inside the reader, and on the
/// seal path the reader is the writer thread. Return `Ok(None)`.
/// Run an encoder over a run of WAL rows and validate the result.
///
/// The shared enforcement point for all three callers that build a segment:
/// the writer when it seals, a copy when it carries a live tail across, and a
/// reader materializing a tail. They used to check three different things, and
/// the reader's was the weakest, so an encoder the seal refused was
/// materialized silently on read: the reader and the next seal disagreed about
/// the tail.
///
/// The check is CONTIGUITY, by counting: the rows handed over that fall
/// inside `[first_ts, last_ts]` must number exactly `rows`. A hole anywhere
/// inside the claimed span would be rows that end up in no segment and no
/// WAL, because the prune deletes everything up to `last_ts`. An encoder may
/// still drop a LEADING or TRAILING run — both narrow the span without
/// holing it. `first_ts`/`last_ts` must also lie inside the input's span,
/// and a segment claiming no rows is refused (return `None` instead).
///
/// A panic inside the encoder is the encoder's failure and is returned as
/// [`Error::Encoder`], not propagated: on the writer thread a propagating
/// panic left every handle reporting `WriterGone`. `Ok(None)` for an empty
/// input without calling the encoder at all.
/// What an encoder returns.
///
/// A boxed `std::error::Error` rather than this crate's own type. The failure is
/// the caller's, and stringifying it at the boundary would discard its concrete
/// type. Wrapped in [`Error::Encoder`](crate::Error), which keeps it as
/// `source()`, so a caller can downcast back to its own error rather than
/// matching on a message it built.
pub type EncodeResult =
Result;
/// Refuse a source written by a different encoder version than `encoder`
/// reports. Either side reporting nothing is not a mismatch: an encoder
/// that does not version itself, or a source from before the key, is
/// unchecked.
/// Encode one `RecordBatch` as a segment's parquet bytes, with the archive's
/// writer properties. The usual last step of a [`SegmentEncoder::encode`].
/// The parquet writer properties every segment in an archive is written with.
///
/// **Compression: LZ4.** Segment columns are already RLE- and bit-packed by the
/// parquet encoders, so an entropy coder has little left to find; LZ4 is where
/// the ratio curve flattens, and it pays for its own encode by shrinking the
/// BLOB the segment insert then writes. Stronger codecs are rejected on
/// *memory*, not ratio or CPU: zstd's compression contexts are per column
/// writer, and a wide stream instantiates thousands of those at once (below).
/// `LZ4_RAW` rather than legacy `LZ4` because the legacy variant is a
/// Hadoop-framed encoding parquet-rs writes only for pre-2.9.0 readers.
///
/// The codec has no bearing on read speed even though it halves the archive;
/// query time tracks segment *count*, which is [`crate::seal`]'s business, not
/// this function's.
///
/// **Dictionary encoding: off, and this is the largest memory decision here.**
/// `ArrowWriter` instantiates a column writer for every column of a row group
/// simultaneously, each carrying its own `DictEncoder` buffer and interner. A
/// wide stream makes that dominant — thousands of columns is not unusual once
/// each value column carries sidecars — so dictionary state, not row data, sets
/// peak RSS during a seal.
///
/// It costs nothing to disable for the numeric columns this format is built
/// for: a monotonic counter makes every value distinct, so the dictionary
/// grows as large as the column it encodes. A caller that puts string data
/// in a segment pays for that on its behalf: repeated values a dictionary
/// would have collapsed are written out in full.
///
/// **Deliberately left at parquet-rs defaults:** `write_batch_size`,
/// statistics granularity, and the page-size limits. Each looks like a bound
/// on per-column-writer memory and none of them measurably is, while
/// chunk-level statistics costs finalize latency and read pruning. The
/// dictionary is the whole effect.