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}