1use std::num::NonZeroUsize;
20use std::sync::{
21 Arc,
22 atomic::{AtomicBool, Ordering},
23};
24
25use arrow_array::RecordBatch;
27use tokio::sync::OwnedSemaphorePermit;
28
29use super::execute::Plan;
31use super::object_schemas::NormalizedObjects;
32use super::transform::{ProjectExecution, Transform, project::Project};
33use super::{QueryCompletion, QueryOutput, Source};
34use crate::storage::arrow::{ArrowBatch, ArrowSchema};
35use crate::storage::backend::ScanPage;
36use crate::storage::config::SCAN_ROWS;
37use crate::storage::manager::Inner;
38use crate::storage::selection::Entry;
39use crate::storage::snapshot::Snapshot;
40use crate::storage::{FileReference, ObjectEntry, SchemaKey, StorageManager, StorageSchema, StoredRecord, StoredRow};
41use crate::{Error, MemorySize, Result};
42use harmos_domain::model::storage::ArrowIpc;
43
44const PROTOBUF_FRAMING: usize = 30;
49
50#[derive(Clone, Debug)]
57pub struct Query {
58 pub at: harmos_domain::model::storage::StorageCursor,
59 pub source: Source,
60 pub transforms: Vec<harmos_domain::model::storage::query::transform::Transform>,
61 pub slice: harmos_domain::model::storage::query::RowSlice,
62}
63
64pub struct QueryReader {
65 context: crate::execution::Context,
66 inner: Arc<Inner>,
67 query: Query,
68 snapshot: Option<Snapshot>,
69 active: Option<OwnedSemaphorePermit>,
70 schema: Option<StorageSchema>,
71 objects: Option<NormalizedObjects>,
72 plan: Option<Plan>,
73 projection: Option<(Project, Arc<arrow_schema::Schema>)>,
74 after: u64,
75 rows: NonZeroUsize,
76 source_done: bool,
77 pending: Option<RecordBatch>,
78 pending_offset: usize,
79 delivered: u64,
80 exhausted: bool,
81 failed: bool,
82 inflight: bool,
83}
84
85#[derive(Clone, Debug)]
88pub enum QueryArrow {
89 Schema(ArrowSchema),
90 Batch(ArrowBatch),
91}
92struct CancelWork(Arc<AtomicBool>);
93
94impl Drop for CancelWork {
95 fn drop(&mut self) {
96 self.0.store(true, Ordering::Release);
97 }
98}
99
100enum Rows {
101 Objects(Vec<ObjectEntry>),
102 Records(Vec<StoredRecord>),
103 Stream(Vec<StoredRow>),
104 Schemas(Vec<StorageSchema>),
105 Files(Vec<FileReference>),
106}
107
108impl Query {
113 pub fn validate(&self) -> harmos_domain::Result<()> {
114 self.source.validate()?;
115 if self.transforms.len() > 64 {
116 return Err(harmos_domain::model::storage::query::transform::invalid("too many query transforms"));
117 }
118 Ok(())
119 }
120}
121
122impl MemorySize for Query {
123 fn memory_size(&self) -> usize {
124 std::mem::size_of::<Self>()
125 .saturating_add(self.at.memory_size().saturating_sub(std::mem::size_of_val(&self.at)))
126 .saturating_add(self.source.memory_size().saturating_sub(std::mem::size_of_val(&self.source)))
127 .saturating_add(self.transforms.memory_size().saturating_sub(std::mem::size_of_val(&self.transforms)))
128 .saturating_add(self.slice.memory_size().saturating_sub(std::mem::size_of_val(&self.slice)))
129 }
130}
131
132impl QueryArrow {
133 pub fn output_memory_size(&self) -> usize {
135 let capacity = match self {
136 Self::Schema(schema) => schema.ipc_capacity(),
137 Self::Batch(batch) => batch.ipc_capacity(),
138 };
139 std::mem::size_of::<QueryOutput>().saturating_add(capacity)
140 }
141
142 #[tracing::instrument(name = "storage.query.reader.output", level = "trace", skip_all)]
143 pub fn output(self) -> QueryOutput {
144 match self {
145 Self::Schema(schema) => QueryOutput::Schema(ArrowIpc::from(schema)),
146 Self::Batch(batch) => QueryOutput::Batch(ArrowIpc::from(batch)),
147 }
148 }
149}
150
151impl TryFrom<QueryOutput> for QueryArrow {
152 type Error = Error;
153
154 fn try_from(output: QueryOutput) -> Result<Self> {
155 Ok(match output {
156 QueryOutput::Schema(ipc) => Self::Schema(ArrowSchema::from_ipc(ipc.into_bytes())?),
157 QueryOutput::Batch(ipc) => Self::Batch(ArrowBatch::from_ipc(ipc.into_bytes())?),
158 })
159 }
160}
161
162impl MemorySize for QueryArrow {
163 fn memory_size(&self) -> usize {
164 std::mem::size_of::<Self>()
165 + match self {
166 Self::Schema(value) => value.memory_size().saturating_sub(std::mem::size_of_val(value)),
167 Self::Batch(value) => value.memory_size().saturating_sub(std::mem::size_of_val(value)),
168 }
169 }
170}
171impl<C> StorageManager<C> {
172 #[tracing::instrument(name = "storage.manager.query", level = "trace", skip_all, fields(outcome = "cancelled"))]
173 pub async fn query(&self, query: Query) -> Result<QueryReader> {
174 let result = async {
175 query.validate()?;
176 self.inner.primary.history().check(&query.at.history)?;
177
178 let active = query_slot(&self.inner).await?;
179
180 let snapshot = self.snapshot(&query.at).await?;
182 let after = match &query.source {
183 Source::Stream(selection) => selection.after,
184 _ => 0,
185 };
186 Ok(QueryReader {
187 context: crate::execution::Context::current(),
188 inner: Arc::clone(&self.inner),
189 query,
190 snapshot: Some(snapshot),
191 active: Some(active),
192 schema: None,
193 objects: None,
194 plan: None,
195 projection: None,
196 after,
197 rows: SCAN_ROWS,
198 source_done: false,
199 pending: None,
200 pending_offset: 0,
201 delivered: 0,
202 exhausted: false,
203 failed: false,
204 inflight: false,
205 })
206 }
207 .await;
208 tracing::Span::current().record("outcome", if result.is_ok() { "completed" } else { "failed" });
209 result
210 }
211}
212
213impl QueryReader {
214 pub async fn next(&mut self) -> Result<Option<QueryArrow>> {
215 let context = self.context.clone();
216 let span = context.sync(|| tracing::trace_span!("storage.reader.page"));
217 let future = async {
218 if self.failed || self.inflight {
219 self.fail();
220 return Err(Error::NotReady {
221 domain: "failed or interrupted storage query",
222 });
223 }
224 self.inflight = true;
225 let result = self.advance().await;
226 self.inflight = false;
227 if result.is_err() {
228 self.fail();
229 }
230 result
231 };
232 context.scope(tracing::Instrument::instrument(future, span)).await
233 }
234
235 #[tracing::instrument(name = "storage.query.reader.completion", level = "trace", skip_all)]
236 pub fn completion(&self) -> Result<QueryCompletion> {
237 if !self.exhausted || self.failed || self.inflight {
238 return Err(Error::NotReady { domain: "storage query completion" });
239 }
240 Ok(QueryCompletion {
241 at: self.query.at.clone(),
242 rows: self.delivered,
243 })
244 }
245
246 fn fail(&mut self) {
247 self.failed = true;
248 self.pending = None;
249 self.plan = None;
250 self.schema = None;
251 self.objects = None;
252 self.projection = None;
253 self.release();
254 }
255
256 fn release(&mut self) {
258 self.snapshot = None;
259 self.active = None;
260 }
261
262 #[tracing::instrument(name = "storage.query.reader.advance", level = "trace", skip_all)]
263 async fn advance(&mut self) -> Result<Option<QueryArrow>> {
264 if self.exhausted {
265 return Ok(None);
266 }
267 if self.plan.is_none() {
268 let schema = self.prepare().await?;
269 return Ok(Some(QueryArrow::Schema(schema)));
270 }
271 loop {
272 if let Some(batch) = self.deliver().await? {
273 return Ok(Some(QueryArrow::Batch(batch)));
274 }
275 let plan = self.plan.as_ref().expect("prepared plan");
276 if plan.is_satisfied() {
277 self.source_done = true;
278 }
279 if !self.source_done {
280 self.next_page().await?;
281 continue;
282 }
283 self.snapshot = None;
284 if !self.finish().await? {
285 self.release();
286 self.exhausted = true;
287 return Ok(None);
288 }
289 }
290 }
291
292 #[tracing::instrument(name = "storage.query.reader.prepare", level = "trace", skip_all)]
294 async fn prepare(&mut self) -> Result<ArrowSchema> {
295 let snapshot = self.snapshot.as_ref().expect("prepared query owns its snapshot");
296 let mut transforms = std::mem::take(&mut self.query.transforms);
297 if transforms.len() > super::execute::MAX_TRANSFORMS {
298 return Err(super::transform::invalid("too many query transforms"));
299 }
300 match &self.query.source {
301 Source::Objects(selection) => {
302 let objects = NormalizedObjects::resolve(&self.inner, snapshot, selection, &mut transforms).await?;
303 self.objects = Some(objects);
304 }
305 Source::Records(selection) => {
306 let schema = schema_at(&self.inner, snapshot, &selection.schema).await?;
307 self.schema = Some(schema);
308 }
309 Source::Stream(selection) => {
310 let schema = schema_at(&self.inner, snapshot, &selection.schema).await?;
311 self.schema = Some(schema);
312 }
313 Source::Schemas(_) | Source::Files(_) => {}
314 }
315
316 let mut empty = self.convert(None)?;
317 crate::storage::arrow::validate_schema(&empty.schema())?;
318 if matches!(self.query.source, Source::Records(_) | Source::Stream(_))
320 && let Some(Transform::Project(project)) = transforms.first()
321 && project.fields.iter().all(|field| field.field.fields.first().is_some_and(|name| name == "payload"))
322 {
323 empty = project.apply(&empty, &mut || Ok(()))?;
324 let Transform::Project(project) = transforms.remove(0) else { unreachable!() };
325 self.projection = Some((project, empty.schema()));
326 }
327 let plan = Plan::new(empty.schema(), transforms, self.query.slice)?;
328 let schema = ArrowSchema::try_new(plan.output_schema())?;
329 self.plan = Some(plan);
330 Ok(schema)
331 }
332
333 #[tracing::instrument(name = "storage.query.reader.next_page", level = "trace", skip_all, fields(after = self.after, rows = self.rows.get()))]
335 async fn next_page(&mut self) -> Result<()> {
336 self.page_attempt().await
337 }
338
339 #[tracing::instrument(name = "storage.query.reader.page_attempt", level = "trace", skip_all)]
340 async fn page_attempt(&mut self) -> Result<()> {
341 let scan_bytes = self.inner.settings.scan_bytes();
342 let page = ScanPage {
343 after: self.after,
344 rows: self.rows,
345 bytes: scan_bytes,
346 };
347
348 let snapshot = self.snapshot.as_ref().expect("running query owns its snapshot");
349
350 let (rows, _) = match &self.query.source {
351 Source::Objects(selection) => scan(snapshot, selection, page, Rows::Objects).await?,
352 Source::Records(selection) => scan(snapshot, selection, page, Rows::Records).await?,
353 Source::Stream(selection) => scan(snapshot, selection, page, Rows::Stream).await?,
354 Source::Schemas(selection) => scan(snapshot, selection, page, Rows::Schemas).await?,
355 Source::Files(selection) => scan(snapshot, selection, page, Rows::Files).await?,
356 };
357
358 let Some(last) = rows.last() else {
359 self.source_done = true;
360 return Ok(());
361 };
362
363 let batch = self.convert(Some(&rows))?;
365 drop(rows);
366 let output = self.work(Some(batch)).await?;
367 self.pending = output.filter(|output| output.num_rows() != 0);
368 self.pending_offset = 0;
369 self.after = last;
370 Ok(())
371 }
372
373 #[tracing::instrument(name = "storage.query.reader.finish", level = "trace", skip_all)]
375 async fn finish(&mut self) -> Result<bool> {
376 if self.plan.is_none() {
377 return Ok(false);
378 }
379 match self.work(None).await? {
380 Some(output) if output.num_rows() != 0 => {
381 self.pending = Some(output);
382 self.pending_offset = 0;
383 Ok(true)
384 }
385 _ => Ok(false),
386 }
387 }
388
389 async fn work(&mut self, batch: Option<RecordBatch>) -> Result<Option<RecordBatch>> {
391 let mut plan = self.plan.take().expect("prepared query plan");
392 let cancelled = Arc::new(AtomicBool::new(false));
393 let guard = CancelWork(Arc::clone(&cancelled));
394 let context = crate::execution::Context::current();
395 let result = tokio::task::spawn_blocking(move || {
396 context.blocking(|| {
397 let mut check = || {
398 if cancelled.load(Ordering::Acquire) { Err(Error::Closing { domain: "cancelled storage query" }) } else { Ok(()) }
399 };
400 let output = match batch {
401 Some(batch) => plan.page(&batch, &mut check),
402 None => plan.finish(&mut check),
403 }?;
404 Ok::<_, Error>((plan, output))
405 })
406 })
407 .await
408 .map_err(|error| Error::backend("query worker", crate::BackendFailure::Permanent, error))??;
409 drop(guard);
410 self.plan = Some(result.0);
411 Ok(result.1)
412 }
413
414 #[tracing::instrument(name = "storage.query.reader.deliver", level = "trace", skip_all)]
416 async fn deliver(&mut self) -> Result<Option<ArrowBatch>> {
417 let Some(batch) = &self.pending else {
418 return Ok(None);
419 };
420 if self.pending_offset >= batch.num_rows() {
421 self.pending = None;
422 self.pending_offset = 0;
423 return Ok(None);
424 }
425 let settings = self.inner.settings.response;
426 let mut rows = settings.batch_rows.get().min(batch.num_rows() - self.pending_offset);
427 loop {
428 let slice = batch.slice(self.pending_offset, rows);
429 let output = ArrowBatch::try_new(slice)?;
430
431 let envelope = QueryOutput::Batch(ArrowIpc::new(output.ipc_bytes().to_vec()));
433 let encoded = crate::Message::encoded_size(&envelope).saturating_add(PROTOBUF_FRAMING);
436 drop(envelope);
437 if encoded <= settings.batch_bytes.get() {
438 self.pending_offset += rows;
439 self.delivered += rows as u64;
440 return Ok(Some(output));
441 }
442 if rows == 1 {
443 return Err(Error::ResourceExhausted { resource: "oversized query row" });
444 }
445 rows /= 2;
446 }
447 }
448
449 #[tracing::instrument(name = "storage.query.reader.convert", level = "trace", skip_all)]
450 fn convert(&self, rows: Option<&Rows>) -> Result<RecordBatch> {
451 let schema = self.schema.as_ref();
452 match (&self.query.source, rows) {
453 (Source::Objects(_), rows) => {
454 let objects = self.objects.as_ref().expect("resolved object schemas");
455 match rows {
456 Some(Rows::Objects(rows)) => objects.map(rows),
457 _ => Ok(objects.empty()),
458 }
459 }
460 (Source::Records(_), Some(Rows::Records(rows))) => match &self.projection {
461 Some((project, output)) => super::source::projected(schema.expect("record schema"), rows.iter(), project, output.clone()),
462 None => super::source::records(schema.expect("record schema"), rows),
463 },
464 (Source::Records(_), _) => super::source::records(schema.expect("record schema"), &[]),
465 (Source::Stream(_), Some(Rows::Stream(rows))) => match &self.projection {
466 Some((project, output)) => {
467 let records = rows.iter().map(|row| &row.record);
468 super::source::projected(schema.expect("stream schema"), records, project, output.clone())
469 }
470 None => super::source::stream(schema.expect("stream schema"), rows),
471 },
472 (Source::Stream(_), _) => super::source::stream(schema.expect("stream schema"), &[]),
473 (Source::Schemas(_), Some(Rows::Schemas(rows))) => super::source::schemas(rows),
474 (Source::Schemas(_), _) => super::source::schemas(&[]),
475 (Source::Files(_), Some(Rows::Files(rows))) => super::source::files(rows),
476 (Source::Files(_), _) => super::source::files(&[]),
477 }
478 }
479}
480
481impl Rows {
482 fn last(&self) -> Option<u64> {
483 let position = match self {
484 Self::Objects(rows) => rows.last().map(Entry::position),
485 Self::Records(rows) => rows.last().map(Entry::position),
486 Self::Stream(rows) => rows.last().map(Entry::position),
487 Self::Schemas(rows) => rows.last().map(Entry::position),
488 Self::Files(rows) => rows.last().map(Entry::position),
489 };
490 position.map(|position| position.sequence)
491 }
492}
493
494#[tracing::instrument(name = "storage.query.reader.schema_at", level = "trace", skip_all)]
497pub(crate) async fn schema_at(inner: &Arc<Inner>, snapshot: &Snapshot, key: &SchemaKey) -> Result<StorageSchema> {
498 find_schema(inner, snapshot, key).await?.ok_or(Error::InvalidInput {
499 field: "storage schema",
500 reason: "schema is unavailable at requested cursor",
501 })
502}
503
504#[tracing::instrument(name = "storage.query.reader.find_schema", level = "trace", skip_all)]
505pub(crate) async fn find_schema(inner: &Arc<Inner>, snapshot: &Snapshot, key: &SchemaKey) -> Result<Option<StorageSchema>> {
506 let selection = super::SchemaSelection { schema: Some(key.clone()) };
507 let page = ScanPage {
508 after: 0,
509 rows: NonZeroUsize::MIN,
510 bytes: inner.settings.scan_bytes(),
511 };
512 let (mut schemas, _) = snapshot.scan::<StorageSchema>(&selection, page).await?;
513 Ok(schemas.pop())
514}
515
516#[tracing::instrument(name = "storage.query.reader.query_slot", level = "trace", skip_all)]
517pub(crate) async fn query_slot(inner: &Arc<Inner>) -> Result<OwnedSemaphorePermit> {
518 if let Ok(active) = Arc::clone(&inner.query_active).try_acquire_owned() {
519 return Ok(active);
520 }
521 let _pending = Arc::clone(&inner.query_pending)
522 .try_acquire_owned()
523 .map_err(|_| Error::ResourceExhausted { resource: "pending storage queries" })?;
524 Arc::clone(&inner.query_active).acquire_owned().await.map_err(|_| Error::Closing { domain: "storage queries" })
525}
526
527#[tracing::instrument(name = "storage.query.reader.scan", level = "trace", skip_all)]
530async fn scan<T: Entry>(snapshot: &Snapshot, selection: &T::Selection, page: ScanPage, wrap: fn(Vec<T>) -> Rows) -> Result<(Rows, usize)> {
531 let (entries, bytes) = snapshot.scan::<T>(selection, page).await?;
532 Ok((wrap(entries), bytes))
533}
534
535