vortex_layout/strategy.rs
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::sync::Arc;
5use std::sync::atomic::AtomicU64;
6use std::sync::atomic::Ordering;
7
8use async_trait::async_trait;
9use futures::StreamExt;
10use vortex_array::ArrayContext;
11use vortex_array::ArrayId;
12use vortex_array::normalize::NormalizeOptions;
13use vortex_array::normalize::Operation;
14use vortex_error::VortexResult;
15use vortex_session::VortexSession;
16use vortex_utils::aliases::hash_set::HashSet;
17
18use crate::LayoutRef;
19use crate::segments::SegmentSinkRef;
20use crate::sequence::SendableSequentialStream;
21use crate::sequence::SequencePointer;
22use crate::sequence::SequentialStreamAdapter;
23use crate::sequence::SequentialStreamExt;
24
25/// A shared counter of the bytes that layout strategies are holding but have not yet emitted.
26///
27/// Clones share the same counter, so a tracker can be handed to a writer before the write begins
28/// and polled while it runs. Strategies report their own retained bytes with
29/// [`Self::reserve`], which releases the reservation on drop.
30#[derive(Clone, Debug, Default)]
31pub struct BufferedBytesTracker(Arc<AtomicU64>);
32
33impl BufferedBytesTracker {
34 /// Creates a tracker with a zeroed counter.
35 pub fn new() -> Self {
36 Self::default()
37 }
38
39 /// Returns the number of bytes currently retained by layout strategies.
40 pub fn buffered_bytes(&self) -> u64 {
41 self.0.load(Ordering::Relaxed)
42 }
43
44 /// Records `bytes` as buffered until the returned reservation is dropped.
45 pub fn reserve(&self, bytes: u64) -> BufferedBytesReservation {
46 self.0.fetch_add(bytes, Ordering::Relaxed);
47 BufferedBytesReservation {
48 tracker: self.clone(),
49 bytes,
50 }
51 }
52}
53
54/// An outstanding claim on a [`BufferedBytesTracker`], released when dropped.
55#[derive(Debug)]
56pub struct BufferedBytesReservation {
57 tracker: BufferedBytesTracker,
58 bytes: u64,
59}
60
61impl BufferedBytesReservation {
62 /// Returns the number of bytes held by this reservation.
63 pub fn bytes(&self) -> u64 {
64 self.bytes
65 }
66}
67
68impl Drop for BufferedBytesReservation {
69 fn drop(&mut self) {
70 self.tracker.0.fetch_sub(self.bytes, Ordering::Relaxed);
71 }
72}
73
74/// State shared by every strategy participating in a single layout write.
75///
76/// Clones share the [`BufferedBytesTracker`] while retaining the array serialization context.
77/// Passing this context through the strategy tree keeps writer-scoped state independent of the
78/// strategy instances, which may be shared by multiple leaves or writers.
79#[derive(Clone)]
80pub struct LayoutWriterContext {
81 array_ctx: ArrayContext,
82 buffered_bytes: BufferedBytesTracker,
83}
84
85impl LayoutWriterContext {
86 /// Creates a context for a layout write with a fresh buffered bytes tracker.
87 pub fn new(array_ctx: ArrayContext) -> Self {
88 Self {
89 array_ctx,
90 buffered_bytes: BufferedBytesTracker::new(),
91 }
92 }
93
94 /// Replaces the buffered bytes tracker, so callers can observe the counter from outside the
95 /// strategy tree.
96 pub fn with_buffered_bytes_tracker(mut self, tracker: BufferedBytesTracker) -> Self {
97 self.buffered_bytes = tracker;
98 self
99 }
100
101 /// Returns the array serialization context.
102 pub fn array_ctx(&self) -> &ArrayContext {
103 &self.array_ctx
104 }
105
106 /// Returns the tracker that accounts for bytes retained by layout strategies.
107 pub fn buffered_bytes_tracker(&self) -> &BufferedBytesTracker {
108 &self.buffered_bytes
109 }
110
111 /// Returns the number of bytes currently retained by layout strategies.
112 pub fn buffered_bytes(&self) -> u64 {
113 self.buffered_bytes.buffered_bytes()
114 }
115
116 /// Records `bytes` as retained by this write until the returned reservation is dropped.
117 pub fn reserve_buffered_bytes(&self, bytes: u64) -> BufferedBytesReservation {
118 self.buffered_bytes.reserve(bytes)
119 }
120}
121
122impl From<ArrayContext> for LayoutWriterContext {
123 fn from(array_ctx: ArrayContext) -> Self {
124 Self::new(array_ctx)
125 }
126}
127
128/// Writes an ordered array stream into a layout tree and segment sink.
129///
130/// Layout strategies are writer-side extension points. Strategies may repartition, buffer,
131/// collect columns, compute statistics, compress arrays, or delegate to child strategies before
132/// finally emitting segments. They must preserve the logical row order represented by the
133/// [`SequencePointer`]s in the input stream.
134#[async_trait]
135pub trait LayoutStrategy: 'static + Send + Sync {
136 /// Asynchronously process an ordered stream of array chunks, emitting them into a sink and
137 /// returning the [`Layout`][crate::Layout] instance that can be parsed to retrieve the data
138 /// from rest.
139 ///
140 /// This trait uses the `#[async_trait]` attribute to denote that trait objects of this type
141 /// can be `Box`ed or `Arc`ed and shared around. Commonly, these strategies are composed to
142 /// form a operator of operations, each of which modifies the chunk stream in some way before
143 /// passing the data on to a downstream writer.
144 ///
145 /// # Sequencing and EOF
146 ///
147 /// The `stream` parameter is a stream of ordered array chunks, each of which is associated
148 /// with a sequence pointer that indicates its position in the overall array. By passing
149 /// around these pointers (essentially vector clocks), the writer can support concurrent
150 /// and parallel processing while maintaining a deterministic order of data in the file.
151 /// The `ctx` parameter carries both array serialization state and writer-scoped accounting
152 /// through every child strategy.
153 ///
154 /// The `eof` parameter is a guaranteed to be greater than all sequence pointers in the stream.
155 ///
156 /// Because child strategies can write to the end-of-file pointer, it is very important that
157 /// **all strategies must await all children concurrently**. Otherwise it is possible to
158 /// deadlock if one child is waiting to write to EOF while your strategy is preventing the
159 /// stream from progressing to completion.
160 ///
161 /// # Blocking operations
162 ///
163 /// This is an async trait method, which will return a `BoxFuture` that you can await from
164 /// any runtime. Implementations should avoid directly performing blocking work within the
165 /// `write_stream`, and should instead spawn it onto an appropriate runtime or threadpool
166 /// dedicated to such work.
167 ///
168 /// Such operations are common, and include things like compression and parsing large blobs
169 /// of data, or serializing very large messages to flatbuffers.
170 async fn write_stream(
171 &self,
172 ctx: LayoutWriterContext,
173 segment_sink: SegmentSinkRef,
174 stream: SendableSequentialStream,
175 eof: SequencePointer,
176 session: &VortexSession,
177 ) -> VortexResult<LayoutRef>;
178}
179
180/// A layout strategy wrapper that rejects arrays containing encodings outside an allow-list.
181///
182/// Canonical encodings are always permitted. Every chunk is recursively validated before it is
183/// passed to the wrapped strategy.
184#[derive(Clone)]
185pub struct LayoutStrategyEncodingValidator {
186 child: Arc<dyn LayoutStrategy>,
187 allowed_encodings: Arc<HashSet<ArrayId>>,
188}
189
190impl LayoutStrategyEncodingValidator {
191 /// Creates a validator around `child` using the supplied encoding allow-list.
192 pub fn new<S: LayoutStrategy>(child: S, allowed_encodings: HashSet<ArrayId>) -> Self {
193 Self {
194 child: Arc::new(child),
195 allowed_encodings: Arc::new(allowed_encodings),
196 }
197 }
198}
199
200#[async_trait]
201impl LayoutStrategy for LayoutStrategyEncodingValidator {
202 async fn write_stream(
203 &self,
204 ctx: LayoutWriterContext,
205 segment_sink: SegmentSinkRef,
206 stream: SendableSequentialStream,
207 eof: SequencePointer,
208 session: &VortexSession,
209 ) -> VortexResult<LayoutRef> {
210 let dtype = stream.dtype().clone();
211 let allowed_encodings = Arc::clone(&self.allowed_encodings);
212 let stream = stream.map(move |chunk| {
213 let (sequence_id, chunk) = chunk?;
214 let chunk = chunk.normalize(&mut NormalizeOptions {
215 allowed: &allowed_encodings,
216 operation: Operation::Error,
217 })?;
218 Ok((sequence_id, chunk))
219 });
220
221 self.child
222 .write_stream(
223 ctx,
224 segment_sink,
225 SequentialStreamAdapter::new(dtype, stream).sendable(),
226 eof,
227 session,
228 )
229 .await
230 }
231}
232
233#[async_trait]
234impl LayoutStrategy for Arc<dyn LayoutStrategy> {
235 async fn write_stream(
236 &self,
237 ctx: LayoutWriterContext,
238 segment_sink: SegmentSinkRef,
239 stream: SendableSequentialStream,
240 eof: SequencePointer,
241 session: &VortexSession,
242 ) -> VortexResult<LayoutRef> {
243 (**self)
244 .write_stream(ctx, segment_sink, stream, eof, session)
245 .await
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use crate::strategy::BufferedBytesTracker;
252
253 #[test]
254 fn reservations_accumulate_and_release() {
255 let tracker = BufferedBytesTracker::new();
256 assert_eq!(tracker.buffered_bytes(), 0);
257
258 let first = tracker.reserve(16);
259 let second = tracker.reserve(32);
260 assert_eq!(tracker.buffered_bytes(), 48);
261 assert_eq!(first.bytes(), 16);
262
263 drop(first);
264 assert_eq!(tracker.buffered_bytes(), 32);
265
266 drop(second);
267 assert_eq!(tracker.buffered_bytes(), 0);
268 }
269
270 #[test]
271 fn clones_share_the_same_counter() {
272 let tracker = BufferedBytesTracker::new();
273 let observer = tracker.clone();
274
275 let reservation = tracker.reserve(8);
276 assert_eq!(observer.buffered_bytes(), 8);
277
278 drop(reservation);
279 assert_eq!(observer.buffered_bytes(), 0);
280 }
281}