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 vortex_array::ArrayContext;
10use vortex_array::aggregate_fn::AggregateFnId;
11use vortex_error::VortexResult;
12use vortex_session::VortexSession;
13use vortex_utils::aliases::hash_set::HashSet;
14
15use crate::LayoutRef;
16use crate::segments::SegmentSinkRef;
17use crate::sequence::SendableSequentialStream;
18use crate::sequence::SequencePointer;
19
20/// A shared counter of the bytes that layout strategies are holding but have not yet emitted.
21///
22/// Clones share the same counter, so a tracker can be handed to a writer before the write begins
23/// and polled while it runs. Strategies report their own retained bytes with
24/// [`Self::reserve`], which releases the reservation on drop.
25#[derive(Clone, Debug, Default)]
26pub struct BufferedBytesTracker(Arc<AtomicU64>);
27
28impl BufferedBytesTracker {
29    /// Creates a tracker with a zeroed counter.
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Returns the number of bytes currently retained by layout strategies.
35    pub fn buffered_bytes(&self) -> u64 {
36        self.0.load(Ordering::Relaxed)
37    }
38
39    /// Records `bytes` as buffered until the returned reservation is dropped.
40    pub fn reserve(&self, bytes: u64) -> BufferedBytesReservation {
41        self.0.fetch_add(bytes, Ordering::Relaxed);
42        BufferedBytesReservation {
43            tracker: self.clone(),
44            bytes,
45        }
46    }
47}
48
49/// An outstanding claim on a [`BufferedBytesTracker`], released when dropped.
50#[derive(Debug)]
51pub struct BufferedBytesReservation {
52    tracker: BufferedBytesTracker,
53    bytes: u64,
54}
55
56impl BufferedBytesReservation {
57    /// Returns the number of bytes held by this reservation.
58    pub fn bytes(&self) -> u64 {
59        self.bytes
60    }
61}
62
63impl Drop for BufferedBytesReservation {
64    fn drop(&mut self) {
65        self.tracker.0.fetch_sub(self.bytes, Ordering::Relaxed);
66    }
67}
68
69/// State shared by every strategy participating in a single layout write.
70///
71/// Clones share the [`BufferedBytesTracker`] while retaining the array serialization context.
72/// Passing this context through the strategy tree keeps writer-scoped state independent of the
73/// strategy instances, which may be shared by multiple leaves or writers.
74#[derive(Clone)]
75pub struct LayoutWriterContext {
76    array_ctx: ArrayContext,
77    allowed_aggregates: Option<Arc<HashSet<AggregateFnId>>>,
78    nullable_ancestor: bool,
79    buffered_bytes: BufferedBytesTracker,
80}
81
82impl LayoutWriterContext {
83    /// Creates a context for a layout write with a fresh buffered bytes tracker.
84    pub fn new(array_ctx: ArrayContext) -> Self {
85        Self {
86            array_ctx,
87            allowed_aggregates: None,
88            nullable_ancestor: false,
89            buffered_bytes: BufferedBytesTracker::new(),
90        }
91    }
92
93    /// Restrict the aggregate functions this write may record, e.g. in a zone map.
94    ///
95    /// A write that would record an aggregate outside `allowed` fails, matching the array and
96    /// layout contexts: a silently thinner zone map is a file that prunes worse than the
97    /// caller asked for, with nothing in the output saying so. The id set is a plain set of
98    /// ids — callers that source it from editions resolve it themselves.
99    pub fn with_allowed_aggregates(mut self, allowed: HashSet<AggregateFnId>) -> Self {
100        self.allowed_aggregates = Some(Arc::new(allowed));
101        self
102    }
103
104    /// Returns whether `aggregate` may be recorded by this write. Unrestricted contexts
105    /// permit every aggregate.
106    pub fn allows_aggregate(&self, aggregate: &AggregateFnId) -> bool {
107        self.allowed_aggregates
108            .as_ref()
109            .is_none_or(|allowed| allowed.contains(aggregate))
110    }
111
112    /// Marks this write as nested under a nullable container, such as a field of a nullable
113    /// struct.
114    ///
115    /// Container writers store their children unmasked: a field holds a value even in rows where
116    /// the containing struct is null, and the struct's validity is written separately. Statistics
117    /// computed over such a child therefore see values the logical column does not contain, so
118    /// writers that record statistics must omit them rather than record inexact values.
119    pub fn with_nullable_ancestor(mut self) -> Self {
120        self.nullable_ancestor = true;
121        self
122    }
123
124    /// Returns whether a nullable container above this write may hide some of its values.
125    pub fn has_nullable_ancestor(&self) -> bool {
126        self.nullable_ancestor
127    }
128
129    /// Replaces the buffered bytes tracker, so callers can observe the counter from outside the
130    /// strategy tree.
131    pub fn with_buffered_bytes_tracker(mut self, tracker: BufferedBytesTracker) -> Self {
132        self.buffered_bytes = tracker;
133        self
134    }
135
136    /// Returns the array serialization context.
137    pub fn array_ctx(&self) -> &ArrayContext {
138        &self.array_ctx
139    }
140
141    /// Returns the tracker that accounts for bytes retained by layout strategies.
142    pub fn buffered_bytes_tracker(&self) -> &BufferedBytesTracker {
143        &self.buffered_bytes
144    }
145
146    /// Returns the number of bytes currently retained by layout strategies.
147    pub fn buffered_bytes(&self) -> u64 {
148        self.buffered_bytes.buffered_bytes()
149    }
150
151    /// Records `bytes` as retained by this write until the returned reservation is dropped.
152    pub fn reserve_buffered_bytes(&self, bytes: u64) -> BufferedBytesReservation {
153        self.buffered_bytes.reserve(bytes)
154    }
155}
156
157impl From<ArrayContext> for LayoutWriterContext {
158    fn from(array_ctx: ArrayContext) -> Self {
159        Self::new(array_ctx)
160    }
161}
162
163/// Writes an ordered array stream into a layout tree and segment sink.
164///
165/// Layout strategies are writer-side extension points. Strategies may repartition, buffer,
166/// collect columns, compute statistics, compress arrays, or delegate to child strategies before
167/// finally emitting segments. They must preserve the logical row order represented by the
168/// [`SequencePointer`]s in the input stream.
169#[async_trait]
170pub trait LayoutStrategy: 'static + Send + Sync {
171    /// Asynchronously process an ordered stream of array chunks, emitting them into a sink and
172    /// returning the [`Layout`][crate::Layout] instance that can be parsed to retrieve the data
173    /// from rest.
174    ///
175    /// This trait uses the `#[async_trait]` attribute to denote that trait objects of this type
176    /// can be `Box`ed or `Arc`ed and shared around. Commonly, these strategies are composed to
177    /// form a operator of operations, each of which modifies the chunk stream in some way before
178    /// passing the data on to a downstream writer.
179    ///
180    /// # Sequencing and EOF
181    ///
182    /// The `stream` parameter is a stream of ordered array chunks, each of which is associated
183    /// with a sequence pointer that indicates its position in the overall array. By passing
184    /// around these pointers (essentially vector clocks), the writer can support concurrent
185    /// and parallel processing while maintaining a deterministic order of data in the file.
186    ///
187    /// # Writer context
188    ///
189    /// The `ctx` parameter carries array serialization state and writer-scoped accounting, and
190    /// also facts about the position of this write in the layout tree, such as the aggregates the
191    /// write may record and whether a nullable container above it hides some of its values. Those
192    /// facts only reach a child because its parent passed them on, so **strategies must hand the
193    /// `ctx` they received, or a clone of it, to every child they write**. Building a fresh
194    /// [`LayoutWriterContext`] inside a strategy silently discards them.
195    ///
196    /// The `eof` parameter is a guaranteed to be greater than all sequence pointers in the stream.
197    ///
198    /// Because child strategies can write to the end-of-file pointer, it is very important that
199    /// **all strategies must await all children concurrently**. Otherwise it is possible to
200    /// deadlock if one child is waiting to write to EOF while your strategy is preventing the
201    /// stream from progressing to completion.
202    ///
203    /// # Blocking operations
204    ///
205    /// This is an async trait method, which will return a `BoxFuture` that you can await from
206    /// any runtime. Implementations should avoid directly performing blocking work within the
207    /// `write_stream`, and should instead spawn it onto an appropriate runtime or threadpool
208    /// dedicated to such work.
209    ///
210    /// Such operations are common, and include things like compression and parsing large blobs
211    /// of data, or serializing very large messages to flatbuffers.
212    async fn write_stream(
213        &self,
214        ctx: LayoutWriterContext,
215        segment_sink: SegmentSinkRef,
216        stream: SendableSequentialStream,
217        eof: SequencePointer,
218        session: &VortexSession,
219    ) -> VortexResult<LayoutRef>;
220}
221
222#[async_trait]
223impl LayoutStrategy for Arc<dyn LayoutStrategy> {
224    async fn write_stream(
225        &self,
226        ctx: LayoutWriterContext,
227        segment_sink: SegmentSinkRef,
228        stream: SendableSequentialStream,
229        eof: SequencePointer,
230        session: &VortexSession,
231    ) -> VortexResult<LayoutRef> {
232        (**self)
233            .write_stream(ctx, segment_sink, stream, eof, session)
234            .await
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use crate::strategy::BufferedBytesTracker;
241
242    #[test]
243    fn reservations_accumulate_and_release() {
244        let tracker = BufferedBytesTracker::new();
245        assert_eq!(tracker.buffered_bytes(), 0);
246
247        let first = tracker.reserve(16);
248        let second = tracker.reserve(32);
249        assert_eq!(tracker.buffered_bytes(), 48);
250        assert_eq!(first.bytes(), 16);
251
252        drop(first);
253        assert_eq!(tracker.buffered_bytes(), 32);
254
255        drop(second);
256        assert_eq!(tracker.buffered_bytes(), 0);
257    }
258
259    #[test]
260    fn clones_share_the_same_counter() {
261        let tracker = BufferedBytesTracker::new();
262        let observer = tracker.clone();
263
264        let reservation = tracker.reserve(8);
265        assert_eq!(observer.buffered_bytes(), 8);
266
267        drop(reservation);
268        assert_eq!(observer.buffered_bytes(), 0);
269    }
270}