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    buffered_bytes: BufferedBytesTracker,
79}
80
81impl LayoutWriterContext {
82    /// Creates a context for a layout write with a fresh buffered bytes tracker.
83    pub fn new(array_ctx: ArrayContext) -> Self {
84        Self {
85            array_ctx,
86            allowed_aggregates: None,
87            buffered_bytes: BufferedBytesTracker::new(),
88        }
89    }
90
91    /// Restrict the aggregate functions this write may record, e.g. in a zone map.
92    ///
93    /// A write that would record an aggregate outside `allowed` fails, matching the array and
94    /// layout contexts: a silently thinner zone map is a file that prunes worse than the
95    /// caller asked for, with nothing in the output saying so. The id set is a plain set of
96    /// ids — callers that source it from editions resolve it themselves.
97    pub fn with_allowed_aggregates(mut self, allowed: HashSet<AggregateFnId>) -> Self {
98        self.allowed_aggregates = Some(Arc::new(allowed));
99        self
100    }
101
102    /// Returns whether `aggregate` may be recorded by this write. Unrestricted contexts
103    /// permit every aggregate.
104    pub fn allows_aggregate(&self, aggregate: &AggregateFnId) -> bool {
105        self.allowed_aggregates
106            .as_ref()
107            .is_none_or(|allowed| allowed.contains(aggregate))
108    }
109
110    /// Replaces the buffered bytes tracker, so callers can observe the counter from outside the
111    /// strategy tree.
112    pub fn with_buffered_bytes_tracker(mut self, tracker: BufferedBytesTracker) -> Self {
113        self.buffered_bytes = tracker;
114        self
115    }
116
117    /// Returns the array serialization context.
118    pub fn array_ctx(&self) -> &ArrayContext {
119        &self.array_ctx
120    }
121
122    /// Returns the tracker that accounts for bytes retained by layout strategies.
123    pub fn buffered_bytes_tracker(&self) -> &BufferedBytesTracker {
124        &self.buffered_bytes
125    }
126
127    /// Returns the number of bytes currently retained by layout strategies.
128    pub fn buffered_bytes(&self) -> u64 {
129        self.buffered_bytes.buffered_bytes()
130    }
131
132    /// Records `bytes` as retained by this write until the returned reservation is dropped.
133    pub fn reserve_buffered_bytes(&self, bytes: u64) -> BufferedBytesReservation {
134        self.buffered_bytes.reserve(bytes)
135    }
136}
137
138impl From<ArrayContext> for LayoutWriterContext {
139    fn from(array_ctx: ArrayContext) -> Self {
140        Self::new(array_ctx)
141    }
142}
143
144/// Writes an ordered array stream into a layout tree and segment sink.
145///
146/// Layout strategies are writer-side extension points. Strategies may repartition, buffer,
147/// collect columns, compute statistics, compress arrays, or delegate to child strategies before
148/// finally emitting segments. They must preserve the logical row order represented by the
149/// [`SequencePointer`]s in the input stream.
150#[async_trait]
151pub trait LayoutStrategy: 'static + Send + Sync {
152    /// Asynchronously process an ordered stream of array chunks, emitting them into a sink and
153    /// returning the [`Layout`][crate::Layout] instance that can be parsed to retrieve the data
154    /// from rest.
155    ///
156    /// This trait uses the `#[async_trait]` attribute to denote that trait objects of this type
157    /// can be `Box`ed or `Arc`ed and shared around. Commonly, these strategies are composed to
158    /// form a operator of operations, each of which modifies the chunk stream in some way before
159    /// passing the data on to a downstream writer.
160    ///
161    /// # Sequencing and EOF
162    ///
163    /// The `stream` parameter is a stream of ordered array chunks, each of which is associated
164    /// with a sequence pointer that indicates its position in the overall array. By passing
165    /// around these pointers (essentially vector clocks), the writer can support concurrent
166    /// and parallel processing while maintaining a deterministic order of data in the file.
167    /// The `ctx` parameter carries both array serialization state and writer-scoped accounting
168    /// through every child strategy.
169    ///
170    /// The `eof` parameter is a guaranteed to be greater than all sequence pointers in the stream.
171    ///
172    /// Because child strategies can write to the end-of-file pointer, it is very important that
173    /// **all strategies must await all children concurrently**. Otherwise it is possible to
174    /// deadlock if one child is waiting to write to EOF while your strategy is preventing the
175    /// stream from progressing to completion.
176    ///
177    /// # Blocking operations
178    ///
179    /// This is an async trait method, which will return a `BoxFuture` that you can await from
180    /// any runtime. Implementations should avoid directly performing blocking work within the
181    /// `write_stream`, and should instead spawn it onto an appropriate runtime or threadpool
182    /// dedicated to such work.
183    ///
184    /// Such operations are common, and include things like compression and parsing large blobs
185    /// of data, or serializing very large messages to flatbuffers.
186    async fn write_stream(
187        &self,
188        ctx: LayoutWriterContext,
189        segment_sink: SegmentSinkRef,
190        stream: SendableSequentialStream,
191        eof: SequencePointer,
192        session: &VortexSession,
193    ) -> VortexResult<LayoutRef>;
194}
195
196#[async_trait]
197impl LayoutStrategy for Arc<dyn LayoutStrategy> {
198    async fn write_stream(
199        &self,
200        ctx: LayoutWriterContext,
201        segment_sink: SegmentSinkRef,
202        stream: SendableSequentialStream,
203        eof: SequencePointer,
204        session: &VortexSession,
205    ) -> VortexResult<LayoutRef> {
206        (**self)
207            .write_stream(ctx, segment_sink, stream, eof, session)
208            .await
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use crate::strategy::BufferedBytesTracker;
215
216    #[test]
217    fn reservations_accumulate_and_release() {
218        let tracker = BufferedBytesTracker::new();
219        assert_eq!(tracker.buffered_bytes(), 0);
220
221        let first = tracker.reserve(16);
222        let second = tracker.reserve(32);
223        assert_eq!(tracker.buffered_bytes(), 48);
224        assert_eq!(first.bytes(), 16);
225
226        drop(first);
227        assert_eq!(tracker.buffered_bytes(), 32);
228
229        drop(second);
230        assert_eq!(tracker.buffered_bytes(), 0);
231    }
232
233    #[test]
234    fn clones_share_the_same_counter() {
235        let tracker = BufferedBytesTracker::new();
236        let observer = tracker.clone();
237
238        let reservation = tracker.reserve(8);
239        assert_eq!(observer.buffered_bytes(), 8);
240
241        drop(reservation);
242        assert_eq!(observer.buffered_bytes(), 0);
243    }
244}