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}