Skip to main content

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}