• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geo-engine / geoengine / 17036501662

18 Aug 2025 09:22AM UTC coverage: 88.073%. First build
17036501662

Pull #1015

github

web-flow
Merge a8157bfec into db8685e5e
Pull Request #1015: workflow optimization (wip)

1664 of 2176 new or added lines in 51 files covered. (76.47%)

114926 of 130490 relevant lines covered (88.07%)

179859.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.38
/operators/src/cache/cache_operator.rs
1
use super::cache_chunks::CacheElementSpatialBounds;
2
use super::error::CacheError;
3
use super::shared_cache::CacheElement;
4
use crate::adapters::FeatureCollectionChunkMerger;
5
use crate::cache::shared_cache::{AsyncCache, SharedCache};
6
use crate::engine::{
7
    CanonicOperatorName, ChunkByteSize, InitializedRasterOperator, InitializedVectorOperator,
8
    QueryContext, QueryProcessor, RasterResultDescriptor, ResultDescriptor,
9
    TypedRasterQueryProcessor, WorkflowOperatorPath,
10
};
11
use crate::error::Error;
12
use crate::util::Result;
13
use async_trait::async_trait;
14
use futures::stream::{BoxStream, FusedStream};
15
use futures::{Stream, StreamExt, TryStreamExt, ready};
16
use geoengine_datatypes::collections::{FeatureCollection, FeatureCollectionInfos};
17
use geoengine_datatypes::primitives::{
18
    Geometry, QueryAttributeSelection, QueryRectangle, VectorQueryRectangle,
19
};
20
use geoengine_datatypes::raster::{Pixel, RasterTile2D};
21
use geoengine_datatypes::util::arrow::ArrowTyped;
22
use geoengine_datatypes::util::helpers::ge_report;
23
use pin_project::{pin_project, pinned_drop};
24
use std::pin::Pin;
25
use std::task::{Context, Poll};
26
use tokio::sync::mpsc::{UnboundedSender, unbounded_channel};
27

28
/// A cache operator that caches the results of its source operator
29
pub struct InitializedCacheOperator<S> {
30
    source: S,
31
}
32

33
impl<S> InitializedCacheOperator<S> {
34
    pub fn new(source: S) -> Self {
2✔
35
        Self { source }
2✔
36
    }
2✔
37
}
38

39
impl InitializedRasterOperator for InitializedCacheOperator<Box<dyn InitializedRasterOperator>> {
40
    fn result_descriptor(&self) -> &RasterResultDescriptor {
×
41
        self.source.result_descriptor()
×
42
    }
×
43

44
    fn query_processor(&self) -> Result<TypedRasterQueryProcessor> {
2✔
45
        let processor_result = self.source.query_processor();
2✔
46
        match processor_result {
2✔
47
            Ok(p) => {
2✔
48
                let res_processor = match p {
2✔
49
                    TypedRasterQueryProcessor::U8(p) => TypedRasterQueryProcessor::U8(Box::new(
2✔
50
                        CacheQueryProcessor::new(p, self.source.canonic_name()),
2✔
51
                    )),
2✔
52
                    TypedRasterQueryProcessor::U16(p) => TypedRasterQueryProcessor::U16(Box::new(
×
53
                        CacheQueryProcessor::new(p, self.source.canonic_name()),
×
54
                    )),
×
55
                    TypedRasterQueryProcessor::U32(p) => TypedRasterQueryProcessor::U32(Box::new(
×
56
                        CacheQueryProcessor::new(p, self.source.canonic_name()),
×
57
                    )),
×
58
                    TypedRasterQueryProcessor::U64(p) => TypedRasterQueryProcessor::U64(Box::new(
×
59
                        CacheQueryProcessor::new(p, self.source.canonic_name()),
×
60
                    )),
×
61
                    TypedRasterQueryProcessor::I8(p) => TypedRasterQueryProcessor::I8(Box::new(
×
62
                        CacheQueryProcessor::new(p, self.source.canonic_name()),
×
63
                    )),
×
64
                    TypedRasterQueryProcessor::I16(p) => TypedRasterQueryProcessor::I16(Box::new(
×
65
                        CacheQueryProcessor::new(p, self.source.canonic_name()),
×
66
                    )),
×
67
                    TypedRasterQueryProcessor::I32(p) => TypedRasterQueryProcessor::I32(Box::new(
×
68
                        CacheQueryProcessor::new(p, self.source.canonic_name()),
×
69
                    )),
×
70
                    TypedRasterQueryProcessor::I64(p) => TypedRasterQueryProcessor::I64(Box::new(
×
71
                        CacheQueryProcessor::new(p, self.source.canonic_name()),
×
72
                    )),
×
73
                    TypedRasterQueryProcessor::F32(p) => TypedRasterQueryProcessor::F32(Box::new(
×
74
                        CacheQueryProcessor::new(p, self.source.canonic_name()),
×
75
                    )),
×
76
                    TypedRasterQueryProcessor::F64(p) => TypedRasterQueryProcessor::F64(Box::new(
×
77
                        CacheQueryProcessor::new(p, self.source.canonic_name()),
×
78
                    )),
×
79
                };
80
                tracing::debug!(event = "query processor created");
2✔
81
                Ok(res_processor)
2✔
82
            }
83
            Err(err) => {
×
84
                tracing::debug!(event = "query processor failed");
×
85
                Err(err)
×
86
            }
87
        }
88
    }
2✔
89

90
    fn canonic_name(&self) -> CanonicOperatorName {
×
91
        self.source.canonic_name()
×
92
    }
×
93

94
    fn name(&self) -> &'static str {
×
95
        self.source.name()
×
96
    }
×
97

98
    fn path(&self) -> WorkflowOperatorPath {
×
99
        self.source.path()
×
100
    }
×
101

NEW
102
    fn optimize(
×
NEW
103
        &self,
×
NEW
104
        resolution: geoengine_datatypes::primitives::SpatialResolution,
×
NEW
105
    ) -> Result<Box<dyn crate::engine::RasterOperator>, crate::optimization::OptimizationError>
×
106
    {
NEW
107
        self.source.optimize(resolution)
×
NEW
108
    }
×
109
}
110

111
impl InitializedVectorOperator for InitializedCacheOperator<Box<dyn InitializedVectorOperator>> {
112
    fn result_descriptor(&self) -> &crate::engine::VectorResultDescriptor {
×
113
        self.source.result_descriptor()
×
114
    }
×
115

116
    fn query_processor(&self) -> Result<crate::engine::TypedVectorQueryProcessor> {
×
117
        let processor_result = self.source.query_processor();
×
118
        match processor_result {
×
119
            Ok(p) => {
×
120
                let res_processor = match p {
×
121
                    crate::engine::TypedVectorQueryProcessor::Data(p) => {
×
122
                        crate::engine::TypedVectorQueryProcessor::Data(Box::new(
×
123
                            CacheQueryProcessor::new(p, self.source.canonic_name()),
×
124
                        ))
×
125
                    }
126
                    crate::engine::TypedVectorQueryProcessor::MultiPoint(p) => {
×
127
                        crate::engine::TypedVectorQueryProcessor::MultiPoint(Box::new(
×
128
                            CacheQueryProcessor::new(p, self.source.canonic_name()),
×
129
                        ))
×
130
                    }
131
                    crate::engine::TypedVectorQueryProcessor::MultiLineString(p) => {
×
132
                        crate::engine::TypedVectorQueryProcessor::MultiLineString(Box::new(
×
133
                            CacheQueryProcessor::new(p, self.source.canonic_name()),
×
134
                        ))
×
135
                    }
136
                    crate::engine::TypedVectorQueryProcessor::MultiPolygon(p) => {
×
137
                        crate::engine::TypedVectorQueryProcessor::MultiPolygon(Box::new(
×
138
                            CacheQueryProcessor::new(p, self.source.canonic_name()),
×
139
                        ))
×
140
                    }
141
                };
142
                tracing::debug!(event = "query processor created");
×
143

144
                Ok(res_processor)
×
145
            }
146
            Err(err) => {
×
147
                tracing::debug!(event = "query processor failed");
×
148
                Err(err)
×
149
            }
150
        }
151
    }
×
152

153
    fn canonic_name(&self) -> CanonicOperatorName {
×
154
        self.source.canonic_name()
×
155
    }
×
156

157
    fn name(&self) -> &'static str {
×
158
        self.source.name()
×
159
    }
×
160

161
    fn path(&self) -> WorkflowOperatorPath {
×
162
        self.source.path()
×
163
    }
×
164

NEW
165
    fn optimize(
×
NEW
166
        &self,
×
NEW
167
        resolution: geoengine_datatypes::primitives::SpatialResolution,
×
NEW
168
    ) -> Result<Box<dyn crate::engine::VectorOperator>, crate::optimization::OptimizationError>
×
169
    {
NEW
170
        self.source.optimize(resolution)
×
NEW
171
    }
×
172
}
173

174
/// A cache operator that caches the results of its source operator
175
struct CacheQueryProcessor<P, E, Q, U, R>
176
where
177
    E: CacheElement + Send + Sync + 'static,
178
    P: QueryProcessor<Output = E, SpatialBounds = Q, Selection = U, ResultDescription = R>,
179
{
180
    processor: P,
181
    cache_key: CanonicOperatorName,
182
}
183

184
impl<P, E, Q, U, R> CacheQueryProcessor<P, E, Q, U, R>
185
where
186
    E: CacheElement + Send + Sync + 'static,
187
    P: QueryProcessor<Output = E, SpatialBounds = Q, Selection = U, ResultDescription = R> + Sized,
188
{
189
    pub fn new(processor: P, cache_key: CanonicOperatorName) -> Self {
2✔
190
        CacheQueryProcessor {
2✔
191
            processor,
2✔
192
            cache_key,
2✔
193
        }
2✔
194
    }
2✔
195
}
196

197
#[async_trait]
198
impl<P, E, S, U, R> QueryProcessor for CacheQueryProcessor<P, E, S, U, R>
199
where
200
    P: QueryProcessor<Output = E, SpatialBounds = S, Selection = U, ResultDescription = R> + Sized,
201
    S: Clone + Send + Sync + 'static,
202
    U: QueryAttributeSelection,
203
    E: CacheElement<Query = QueryRectangle<S, U>>
204
        + Send
205
        + Sync
206
        + 'static
207
        + ResultStreamWrapper
208
        + Clone,
209
    E::ResultStream: Stream<Item = Result<E, CacheError>> + Send + Sync + 'static,
210
    SharedCache: AsyncCache<E>,
211
    R: ResultDescriptor<QueryRectangleSpatialBounds = S, QueryRectangleAttributeSelection = U>,
212
{
213
    type Output = E;
214
    type SpatialBounds = S;
215
    type Selection = U;
216
    type ResultDescription = R;
217

218
    async fn _query<'a>(
219
        &'a self,
220
        query: QueryRectangle<Self::SpatialBounds, Self::Selection>,
221
        ctx: &'a dyn QueryContext,
222
    ) -> Result<BoxStream<'a, Result<Self::Output>>> {
8✔
223
        let shared_cache = ctx
4✔
224
            .cache()
4✔
225
            .expect("`SharedCache` extension should be set during `ProContext` creation");
4✔
226

227
        let cache_result = shared_cache.query_cache(&self.cache_key, &query).await;
4✔
228

229
        if let Ok(Some(cache_result)) = cache_result {
×
230
            // cache hit
231
            tracing::debug!("cache hit for operator {}", self.cache_key);
×
232

233
            let wrapped_result_steam =
×
234
                E::wrap_result_stream(cache_result, ctx.chunk_byte_size(), query.clone());
×
235

236
            return Ok(wrapped_result_steam);
×
237
        }
4✔
238

239
        // cache miss
240
        tracing::debug!("cache miss for operator {}", self.cache_key);
4✔
241
        let source_stream = self.processor.query(query.clone(), ctx).await?;
4✔
242

243
        let query_id = shared_cache.insert_query(&self.cache_key, &query).await;
4✔
244

245
        if let Err(e) = query_id {
4✔
246
            tracing::debug!("could not insert query into cache: {e}");
×
247
            return Ok(source_stream);
×
248
        }
4✔
249

250
        let query_id = query_id.expect("query_id should be set because of the previous check");
4✔
251

252
        // lazily insert tiles into the cache as they are produced
253
        let (stream_event_sender, mut stream_event_receiver) = unbounded_channel();
4✔
254

255
        let cache_key = self.cache_key.clone();
4✔
256
        let tile_cache = shared_cache.clone();
4✔
257
        crate::util::spawn(async move {
4✔
258
            while let Some(event) = stream_event_receiver.recv().await {
134✔
259
                match event {
132✔
260
                    SourceStreamEvent::Element(tile) => {
130✔
261
                        let result = tile_cache
130✔
262
                            .insert_query_element(&cache_key, &query_id, tile)
130✔
263
                            .await;
130✔
264
                        tracing::trace!(
128✔
265
                            "inserted tile into cache for cache key {cache_key} and query id {query_id}. result: {result:?}"
×
266
                        );
267
                    }
268
                    SourceStreamEvent::Abort => {
269
                        tile_cache.abort_query(&cache_key, &query_id).await;
×
270
                        tracing::debug!(
×
271
                            "aborted cache insertion for cache key {cache_key} and query id {query_id}"
×
272
                        );
273
                    }
274
                    SourceStreamEvent::Finished => {
275
                        let result = tile_cache.finish_query(&cache_key, &query_id).await;
2✔
276
                        tracing::debug!(
2✔
277
                            "finished cache insertion for cache key {cache_key} and query id {query_id}, result: {result:?}"
×
278
                        );
279
                    }
280
                }
281
            }
282
        });
2✔
283

284
        let output_stream = CacheOutputStream {
4✔
285
            source: source_stream,
4✔
286
            stream_event_sender,
4✔
287
            finished: false,
4✔
288
            pristine: true,
4✔
289
        };
4✔
290

291
        Ok(Box::pin(output_stream))
4✔
292
    }
8✔
293

294
    fn result_descriptor(&self) -> &Self::ResultDescription {
8✔
295
        self.processor.result_descriptor()
8✔
296
    }
8✔
297
}
298

299
#[allow(clippy::large_enum_variant)] // TODO: Box instead?
300
enum SourceStreamEvent<E: CacheElement> {
301
    Element(E),
302
    Abort,
303
    Finished,
304
}
305

306
/// Custom stream that lazily puts the produced tile in the cache and finishes the cache entry when the source stream completes
307
#[pin_project(PinnedDrop, project = CacheOutputStreamProjection)]
308
struct CacheOutputStream<S, E>
309
where
310
    S: Stream<Item = Result<E>>,
311
    E: CacheElement + Clone,
312
{
313
    #[pin]
314
    source: S,
315
    stream_event_sender: UnboundedSender<SourceStreamEvent<E>>,
316
    finished: bool,
317
    pristine: bool,
318
}
319

320
impl<S, E> Stream for CacheOutputStream<S, E>
321
where
322
    S: Stream<Item = Result<E>>,
323
    E: CacheElement + Clone,
324
{
325
    type Item = Result<E>;
326

327
    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
421✔
328
        let this = self.project();
421✔
329

330
        let next = ready!(this.source.poll_next(cx));
421✔
331

332
        if let Some(element) = &next {
164✔
333
            *this.pristine = false;
160✔
334
            if let Ok(element) = element {
160✔
335
                // ignore the result. The receiver shold never drop prematurely, but if it does we don't want to crash
336
                let r = this
160✔
337
                    .stream_event_sender
160✔
338
                    .send(SourceStreamEvent::Element(element.clone()));
160✔
339
                if let Err(e) = r {
160✔
340
                    tracing::warn!("could not send tile to cache: {}", ge_report(e));
×
341
                }
160✔
342
            } else {
343
                // ignore the result. The receiver shold never drop prematurely, but if it does we don't want to crash
344
                let r = this.stream_event_sender.send(SourceStreamEvent::Abort);
×
345
                if let Err(e) = r {
×
346
                    tracing::warn!("could not send abort to cache: {}", ge_report(e));
×
347
                }
×
348
            }
349
        } else {
350
            if *this.pristine {
4✔
351
                // ignore the result. The receiver shold never drop prematurely, but if it does we don't want to crash
352
                let r = this.stream_event_sender.send(SourceStreamEvent::Abort);
×
353
                if let Err(e) = r {
×
354
                    tracing::warn!("could not send abort to cache: {}", ge_report(e));
×
355
                }
×
356
            } else {
357
                // ignore the result. The receiver shold never drop prematurely, but if it does we don't want to crash
358
                let r = this.stream_event_sender.send(SourceStreamEvent::Finished);
4✔
359
                if let Err(e) = r {
4✔
360
                    tracing::warn!("could not send finished to cache: {}", ge_report(e));
×
361
                }
4✔
362
                tracing::debug!("stream finished, mark cache entry as finished.");
4✔
363
            }
364
            *this.finished = true;
4✔
365
        }
366

367
        Poll::Ready(next)
164✔
368
    }
421✔
369
}
370

371
/// On drop, trigger the removal of the cache entry if it hasn't been finished yet
372
#[pinned_drop]
373
impl<S, E> PinnedDrop for CacheOutputStream<S, E>
374
where
375
    S: Stream<Item = Result<E>>,
376
    E: CacheElement + Clone,
4✔
377
{
4✔
378
    fn drop(self: Pin<&mut Self>) {
4✔
379
        if !self.finished {
4✔
380
            // ignore the result. The receiver shold never drop prematurely, but if it does we don't want to crash
381
            let r = self.stream_event_sender.send(SourceStreamEvent::Abort);
×
382
            if let Err(e) = r {
×
383
                tracing::debug!("could not send abort to cache: {e}");
×
384
            }
×
385
        }
4✔
386
    }
4✔
387
}
388

389
trait ResultStreamWrapper: CacheElement {
390
    fn wrap_result_stream<'a>(
391
        stream: Self::ResultStream,
392
        chunk_byte_size: ChunkByteSize,
393
        query: Self::Query,
394
    ) -> BoxStream<'a, Result<Self>>;
395
}
396

397
impl<G> ResultStreamWrapper for FeatureCollection<G>
398
where
399
    G: Geometry + ArrowTyped + Send + Sync + 'static,
400
    FeatureCollection<G>:
401
        CacheElement<Query = VectorQueryRectangle> + Send + Sync + CacheElementSpatialBounds,
402
    Self::ResultStream: FusedStream + Send + Sync,
403
{
404
    fn wrap_result_stream<'a>(
×
405
        stream: Self::ResultStream,
×
406
        chunk_byte_size: ChunkByteSize,
×
407
        query: Self::Query,
×
408
    ) -> BoxStream<'a, Result<Self>> {
×
409
        let filter_stream = stream.filter_map(move |result| {
×
410
            let query = query.clone();
×
411
            async move {
×
412
                result
×
413
                    .and_then(|collection| collection.filter_cache_element_entries(&query))
×
414
                    .map_err(|source| Error::CacheCantProduceResult {
×
415
                        source: source.into(),
×
416
                    })
×
417
                    .map(|fc| if fc.is_empty() { None } else { Some(fc) })
×
418
                    .transpose()
×
419
            }
×
420
        });
×
421

422
        let merger_stream =
×
423
            FeatureCollectionChunkMerger::new(filter_stream, chunk_byte_size.into());
×
424
        Box::pin(merger_stream)
×
425
    }
×
426
}
427

428
impl<P> ResultStreamWrapper for RasterTile2D<P>
429
where
430
    P: 'static + Pixel,
431
    RasterTile2D<P>: CacheElement,
432
    Self::ResultStream: Send + Sync,
433
{
434
    fn wrap_result_stream<'a>(
×
435
        stream: Self::ResultStream,
×
436
        _chunk_byte_size: ChunkByteSize,
×
437
        _query: Self::Query,
×
438
    ) -> BoxStream<'a, Result<Self>> {
×
439
        Box::pin(stream.map_err(|ce| Error::CacheCantProduceResult { source: ce.into() }))
×
440
    }
×
441
}
442

443
#[cfg(test)]
444
mod tests {
445
    use super::*;
446

447
    use crate::{
448
        engine::{
449
            ChunkByteSize, MockExecutionContext, MultipleRasterSources, RasterOperator,
450
            SingleRasterSource, WorkflowOperatorPath,
451
        },
452
        processing::{Expression, ExpressionParams, RasterStacker, RasterStackerParams},
453
        source::{GdalSource, GdalSourceParameters},
454
        util::gdal::add_ndvi_dataset,
455
    };
456
    use futures::StreamExt;
457
    use geoengine_datatypes::{
458
        primitives::{BandSelection, RasterQueryRectangle, TimeInterval},
459
        raster::{GridBoundingBox2D, RasterDataType, RenameBands, TilesEqualIgnoringCacheHint},
460
        util::test::TestDefault,
461
    };
462
    use std::sync::Arc;
463

464
    #[tokio::test]
465
    async fn it_caches() {
1✔
466
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
467

468
        let ndvi_id = add_ndvi_dataset(&mut exe_ctx);
1✔
469

470
        let operator = GdalSource {
1✔
471
            params: GdalSourceParameters::new(ndvi_id.clone()),
1✔
472
        }
1✔
473
        .boxed()
1✔
474
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
475
        .await
1✔
476
        .unwrap();
1✔
477

478
        let cached_op = InitializedCacheOperator::new(operator);
1✔
479

480
        let processor = cached_op.query_processor().unwrap().get_u8().unwrap();
1✔
481

482
        let tile_cache = Arc::new(SharedCache::test_default());
1✔
483

484
        let query_ctx = exe_ctx.mock_query_context_with_query_extensions(
1✔
485
            ChunkByteSize::test_default(),
1✔
486
            Some(tile_cache),
1✔
487
            None,
1✔
488
            None,
1✔
489
        );
490

491
        let stream = processor
1✔
492
            .query(
1✔
493
                RasterQueryRectangle::new(
1✔
494
                    GridBoundingBox2D::new([-90, -180], [89, 179]).unwrap(),
1✔
495
                    TimeInterval::default(),
1✔
496
                    BandSelection::first(),
1✔
497
                ),
1✔
498
                &query_ctx,
1✔
499
            )
1✔
500
            .await
1✔
501
            .unwrap();
1✔
502

503
        let tiles = stream.collect::<Vec<_>>().await;
1✔
504
        let tiles = tiles.into_iter().collect::<Result<Vec<_>>>().unwrap();
1✔
505

506
        // wait for the cache to be filled, which happens asynchronously
507
        tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
1✔
508

509
        // delete the dataset to make sure the result is served from the cache
510
        exe_ctx.delete_meta_data(&ndvi_id);
1✔
511

512
        let stream_from_cache = processor
1✔
513
            .query(
1✔
514
                RasterQueryRectangle::new(
1✔
515
                    GridBoundingBox2D::new([-90, -180], [89, 179]).unwrap(),
1✔
516
                    TimeInterval::default(),
1✔
517
                    BandSelection::first(),
1✔
518
                ),
1✔
519
                &query_ctx,
1✔
520
            )
1✔
521
            .await
1✔
522
            .unwrap();
1✔
523

524
        let tiles_from_cache = stream_from_cache.collect::<Vec<_>>().await;
1✔
525
        let tiles_from_cache = tiles_from_cache
1✔
526
            .into_iter()
1✔
527
            .collect::<Result<Vec<_>>>()
1✔
528
            .unwrap();
1✔
529

530
        assert!(tiles.tiles_equal_ignoring_cache_hint(&tiles_from_cache));
1✔
531
    }
1✔
532

533
    #[tokio::test]
534
    async fn it_reuses_bands_from_cache_entries() {
1✔
535
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
536

537
        let ndvi_id = add_ndvi_dataset(&mut exe_ctx);
1✔
538

539
        let operator = RasterStacker {
1✔
540
            params: RasterStackerParams {
1✔
541
                rename_bands: RenameBands::Default,
1✔
542
            },
1✔
543
            sources: MultipleRasterSources {
1✔
544
                rasters: vec![
1✔
545
                    GdalSource {
1✔
546
                        params: GdalSourceParameters {
1✔
547
                            data: ndvi_id.clone(),
1✔
548
                            overview_level: None,
1✔
549
                        },
1✔
550
                    }
1✔
551
                    .boxed(),
1✔
552
                    Expression {
1✔
553
                        params: ExpressionParams {
1✔
554
                            expression: "2 * A".to_string(),
1✔
555
                            output_type: RasterDataType::U8,
1✔
556
                            output_band: None,
1✔
557
                            map_no_data: false,
1✔
558
                        },
1✔
559
                        sources: SingleRasterSource {
1✔
560
                            raster: GdalSource {
1✔
561
                                params: GdalSourceParameters {
1✔
562
                                    data: ndvi_id.clone(),
1✔
563
                                    overview_level: None,
1✔
564
                                },
1✔
565
                            }
1✔
566
                            .boxed(),
1✔
567
                        },
1✔
568
                    }
1✔
569
                    .boxed(),
1✔
570
                ],
1✔
571
            },
1✔
572
        }
1✔
573
        .boxed()
1✔
574
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
575
        .await
1✔
576
        .unwrap();
1✔
577

578
        let cached_op = InitializedCacheOperator::new(operator);
1✔
579

580
        let processor = cached_op.query_processor().unwrap().get_u8().unwrap();
1✔
581

582
        let tile_cache = Arc::new(SharedCache::test_default());
1✔
583

584
        let query_ctx = exe_ctx.mock_query_context_with_query_extensions(
1✔
585
            ChunkByteSize::test_default(),
1✔
586
            Some(tile_cache),
1✔
587
            None,
1✔
588
            None,
1✔
589
        );
590

591
        // query the first two bands
592
        let stream = processor
1✔
593
            .query(
1✔
594
                RasterQueryRectangle::new(
1✔
595
                    GridBoundingBox2D::new([-90, -180], [89, 179]).unwrap(),
1✔
596
                    TimeInterval::default(),
1✔
597
                    BandSelection::new(vec![0, 1]).unwrap(),
1✔
598
                ),
1✔
599
                &query_ctx,
1✔
600
            )
1✔
601
            .await
1✔
602
            .unwrap();
1✔
603

604
        let tiles = stream.collect::<Vec<_>>().await;
1✔
605
        let tiles = tiles.into_iter().collect::<Result<Vec<_>>>().unwrap();
1✔
606
        // only keep the second band for comparison
607
        let tiles = tiles
1✔
608
            .into_iter()
1✔
609
            .filter_map(|mut tile| {
64✔
610
                if tile.band == 1 {
64✔
611
                    tile.band = 0;
32✔
612
                    Some(tile)
32✔
613
                } else {
614
                    None
32✔
615
                }
616
            })
64✔
617
            .collect::<Vec<_>>();
1✔
618

619
        // wait for the cache to be filled, which happens asynchronously
620
        tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
1✔
621

622
        // delete the dataset to make sure the result is served from the cache
623
        exe_ctx.delete_meta_data(&ndvi_id);
1✔
624

625
        // now query only the second band
626
        let stream_from_cache = processor
1✔
627
            .query(
1✔
628
                RasterQueryRectangle::new(
1✔
629
                    GridBoundingBox2D::new([-90, -180], [89, 179]).unwrap(),
1✔
630
                    TimeInterval::default(),
1✔
631
                    BandSelection::new_single(1),
1✔
632
                ),
1✔
633
                &query_ctx,
1✔
634
            )
1✔
635
            .await
1✔
636
            .unwrap();
1✔
637

638
        let tiles_from_cache = stream_from_cache.collect::<Vec<_>>().await;
1✔
639
        let tiles_from_cache = tiles_from_cache
1✔
640
            .into_iter()
1✔
641
            .collect::<Result<Vec<_>>>()
1✔
642
            .unwrap();
1✔
643

644
        assert!(tiles.tiles_equal_ignoring_cache_hint(&tiles_from_cache));
1✔
645
    }
1✔
646
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc