cognite/api/core/time_series/
datapoints_stream.rs1use std::{
2 collections::{HashMap, VecDeque},
3 pin::Pin,
4 sync::Arc,
5};
6
7use futures::{stream::FuturesUnordered, Stream, StreamExt, TryStream};
8use pin_project::pin_project;
9
10use crate::{
11 send_helper::CondBoxFuture,
12 time_series::{
13 DataPointListItem, DataPointListResponse, DatapointAggregate, DatapointDouble,
14 DatapointString, DatapointsFilter, DatapointsQuery, InstanceId, ListDatapointType,
15 TimeSeriesResource,
16 },
17 Identity, IdentityOrInstance,
18};
19
20pub enum EitherDataPoint {
22 Numeric(DatapointDouble),
24 String(DatapointString),
26 Aggregate(DatapointAggregate),
28}
29
30struct TimeSeriesRef {
31 id: i64,
32 external_id: Option<String>,
33 instance_id: Option<InstanceId>,
34 original_id: IdentityOrInstance,
35 is_string: bool,
36 is_step: bool,
37 unit: Option<String>,
38 unit_external_id: Option<String>,
39}
40
41pub struct DataPointRef {
44 timeseries: Arc<TimeSeriesRef>,
47 datapoint: EitherDataPoint,
48}
49
50impl DataPointRef {
51 pub fn id(&self) -> i64 {
53 self.timeseries.id
54 }
55
56 pub fn external_id(&self) -> Option<&str> {
58 self.timeseries.external_id.as_deref()
59 }
60
61 pub fn instance_id(&self) -> Option<&InstanceId> {
63 self.timeseries.instance_id.as_ref()
64 }
65
66 pub fn original_id(&self) -> &IdentityOrInstance {
68 &self.timeseries.original_id
69 }
70
71 pub fn is_string(&self) -> bool {
73 self.timeseries.is_string
74 }
75
76 pub fn is_step(&self) -> bool {
78 self.timeseries.is_step
79 }
80
81 pub fn unit(&self) -> Option<&str> {
83 self.timeseries.unit.as_deref()
84 }
85
86 pub fn unit_external_id(&self) -> Option<&str> {
88 self.timeseries.unit_external_id.as_deref()
89 }
90
91 pub fn into_datapoint(self) -> EitherDataPoint {
93 self.datapoint
94 }
95
96 pub fn datapoint(&self) -> &EitherDataPoint {
98 &self.datapoint
99 }
100
101 pub fn as_numeric(&self) -> Option<&DatapointDouble> {
103 match &self.datapoint {
104 EitherDataPoint::Numeric(dp) => Some(dp),
105 _ => None,
106 }
107 }
108
109 pub fn as_string(&self) -> Option<&DatapointString> {
111 match &self.datapoint {
112 EitherDataPoint::String(dp) => Some(dp),
113 _ => None,
114 }
115 }
116
117 pub fn as_aggregate(&self) -> Option<&DatapointAggregate> {
119 match &self.datapoint {
120 EitherDataPoint::Aggregate(dp) => Some(dp),
121 _ => None,
122 }
123 }
124}
125
126struct FetchResult {
127 query_items: Vec<DatapointsQuery>,
128 response: DataPointListResponse,
129}
130
131#[derive(Clone, Debug)]
133pub struct DatapointsStreamOptions {
134 pub batch_size: usize,
136 pub parallelism: usize,
138}
139
140impl Default for DatapointsStreamOptions {
141 fn default() -> Self {
142 Self {
143 batch_size: 100,
144 parallelism: 4,
145 }
146 }
147}
148
149pub(super) struct DatapointsStream<'a> {
150 timeseries: &'a TimeSeriesResource,
151 filter: DatapointsFilter,
152 queries: VecDeque<DatapointsQuery>,
153 options: DatapointsStreamOptions,
154 known_timeseries: HashMap<i64, Arc<TimeSeriesRef>>,
155 futures: FuturesUnordered<CondBoxFuture<'a, Result<FetchResult, crate::Error>>>,
159}
160
161impl<'a> DatapointsStream<'a> {
162 pub(super) fn new(
163 timeseries: &'a TimeSeriesResource,
164 mut filter: DatapointsFilter,
165 options: DatapointsStreamOptions,
166 ) -> Self {
167 Self {
168 timeseries,
169 queries: std::mem::take(&mut filter.items).into(),
170 filter,
171 options,
172 known_timeseries: HashMap::new(),
173 futures: FuturesUnordered::new(),
174 }
175 }
176
177 async fn fetch_batch(
178 timeseries: &'a TimeSeriesResource,
179 filter: DatapointsFilter,
180 ) -> Result<FetchResult, crate::Error> {
181 let response = timeseries.retrieve_datapoints_proto(&filter).await?;
182 Ok(FetchResult {
183 query_items: filter.items,
184 response,
185 })
186 }
187
188 fn update_known_timeseries_from_batch(
189 &mut self,
190 response: &DataPointListItem,
191 query: &DatapointsQuery,
192 ) {
193 if self.known_timeseries.contains_key(&response.id) {
195 return;
196 }
197
198 self.known_timeseries.insert(
199 response.id,
200 Arc::new(TimeSeriesRef {
201 id: response.id,
202 external_id: if !response.external_id.is_empty() {
203 Some(response.external_id.clone())
204 } else {
205 None
206 },
207 instance_id: response.instance_id.clone(),
208 original_id: query.id.clone(),
209 is_string: response.is_string,
210 is_step: response.is_step,
211 unit: if !response.unit.is_empty() {
212 Some(response.unit.clone())
213 } else {
214 None
215 },
216 unit_external_id: if !response.unit_external_id.is_empty() {
217 Some(response.unit_external_id.clone())
218 } else {
219 None
220 },
221 }),
222 );
223 }
224
225 fn equals_identity(id: &IdentityOrInstance, response_item: &DataPointListItem) -> bool {
226 match id {
227 IdentityOrInstance::Identity(Identity::Id { id }) => response_item.id == *id,
228 IdentityOrInstance::Identity(Identity::ExternalId { external_id }) => {
229 response_item.external_id == *external_id
230 }
231 IdentityOrInstance::InstanceId { instance_id } => response_item
232 .instance_id
233 .as_ref()
234 .is_some_and(|i| i == instance_id),
235 }
236 }
237
238 async fn stream_batches_inner(
239 &mut self,
240 maintain_internal_state: bool,
241 ) -> Result<Option<DataPointListResponse>, crate::Error> {
242 while self.futures.len() < self.options.parallelism && !self.queries.is_empty() {
244 let mut batch = Vec::with_capacity(self.options.batch_size.min(self.queries.len()));
245 while batch.len() < self.options.batch_size {
246 if let Some(query) = self.queries.pop_front() {
247 batch.push(query);
248 } else {
249 break;
250 }
251 }
252 let filter = DatapointsFilter {
253 items: batch,
254 ..self.filter.clone()
255 };
256 let timeseries = self.timeseries;
257 self.futures
258 .push(Box::pin(Self::fetch_batch(timeseries, filter)));
259 }
260
261 let Some(result) = self.futures.next().await else {
263 return Ok(None);
265 };
266 let mut fetch_result = result?;
267 let mut query_iter = fetch_result.query_items.into_iter();
268
269 for response_item in &mut fetch_result.response.items {
271 let Some(mut query) = query_iter.next() else {
274 return Err(crate::Error::Other(
275 "Internal logic error: more response items than query items".to_string(),
276 ));
277 };
278
279 while !Self::equals_identity(&query.id, response_item) {
280 let Some(next_query) = query_iter.next() else {
283 return Err(crate::Error::Other(
284 "Internal logic error: response item does not match any query item"
285 .to_string(),
286 ));
287 };
288 query = next_query;
289 }
290
291 if maintain_internal_state {
294 self.update_known_timeseries_from_batch(response_item, &query);
295 }
296
297 if !response_item.next_cursor.is_empty() {
298 query.cursor = Some(std::mem::take(&mut response_item.next_cursor));
301 self.queries.push_back(query);
302 }
303 }
304
305 Ok(Some(fetch_result.response))
306 }
307
308 pub fn stream_batches(
309 self,
310 ) -> impl Stream<Item = Result<DataPointListResponse, crate::Error>> + 'a {
311 futures::stream::try_unfold(self, move |mut state| async move {
312 Ok(state.stream_batches_inner(false).await?.map(|v| (v, state)))
313 })
314 }
315
316 pub fn stream_datapoints(self) -> impl Stream<Item = Result<DataPointRef, crate::Error>> + 'a {
317 FlatIterStream::new(futures::stream::try_unfold(
318 self,
319 move |mut state| async move {
320 let Some(batch) = state.stream_batches_inner(true).await? else {
321 return Ok(None);
322 };
323 let mut res = Vec::new();
324
325 for item in batch.items {
326 let timeseries = state
327 .known_timeseries
328 .get(&item.id)
329 .ok_or_else(|| {
330 crate::Error::Other(format!(
331 "Internal logic error: timeseries with id {} not found in known_timeseries",
332 item.id
333 ))
334 })?
335 .clone();
336 match item.datapoint_type {
337 None => continue,
338 Some(ListDatapointType::AggregateDatapoints(dps)) => {
339 res.extend(dps.datapoints.into_iter().map(move |dp| DataPointRef {
340 timeseries: timeseries.clone(),
341 datapoint: EitherDataPoint::Aggregate(dp.into()),
342 }));
343 }
344 Some(ListDatapointType::StringDatapoints(dps)) => {
345 res.extend(dps.datapoints.into_iter().map(move |dp| DataPointRef {
346 timeseries: timeseries.clone(),
347 datapoint: EitherDataPoint::String(dp.into()),
348 }));
349 }
350 Some(ListDatapointType::NumericDatapoints(dps)) => {
351 res.extend(dps.datapoints.into_iter().map(move |dp| DataPointRef {
352 timeseries: timeseries.clone(),
353 datapoint: EitherDataPoint::Numeric(dp.into()),
354 }));
355 }
356 }
357 }
358
359 Ok(Some((res.into_iter(), state)))
360 },
361 ))
362 }
363}
364
365#[pin_project]
366struct FlatIterStream<R>
368where
369 R: TryStream,
370 R::Ok: IntoIterator,
371{
372 #[pin]
373 inner: R,
374 current: Option<<R::Ok as IntoIterator>::IntoIter>,
375}
376
377impl<R> FlatIterStream<R>
378where
379 R: TryStream,
380 R::Ok: IntoIterator,
381{
382 fn new(stream: R) -> Self {
383 Self {
384 inner: stream,
385 current: None,
386 }
387 }
388}
389
390impl<R: TryStream> Stream for FlatIterStream<R>
391where
392 R: TryStream,
393 R::Ok: IntoIterator,
394{
395 type Item = Result<<R::Ok as IntoIterator>::Item, R::Error>;
396
397 fn poll_next(
398 self: Pin<&mut Self>,
399 cx: &mut std::task::Context<'_>,
400 ) -> std::task::Poll<Option<Self::Item>> {
401 let mut this = self.project();
402 loop {
403 if let Some(current) = this.current.as_mut() {
404 if let Some(item) = current.next() {
405 return std::task::Poll::Ready(Some(Ok(item)));
406 } else {
407 *this.current = None;
408 }
409 }
410 match this.inner.as_mut().try_poll_next(cx)? {
411 std::task::Poll::Ready(Some(next_iter)) => {
412 *this.current = Some(next_iter.into_iter());
413 }
414 std::task::Poll::Ready(None) => return std::task::Poll::Ready(None),
415 std::task::Poll::Pending => return std::task::Poll::Pending,
416 }
417 }
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use crate::{
424 api::core::time_series::datapoints_stream::FlatIterStream, time_series::StatusCode,
425 };
426 #[test]
427 fn test_datapoint_ref() {
428 use super::*;
429 let ts_ref = Arc::new(TimeSeriesRef {
430 id: 42,
431 external_id: Some("ts1".to_string()),
432 instance_id: None,
433 original_id: IdentityOrInstance::from("ts1"),
434 is_string: false,
435 is_step: false,
436 unit: Some("°C".to_string()),
437 unit_external_id: None,
438 });
439 let dp = DataPointRef {
440 timeseries: ts_ref.clone(),
441 datapoint: EitherDataPoint::Numeric(DatapointDouble {
442 timestamp: 1625079600000,
443 value: Some(23.5),
444 status: Some(StatusCode::Good),
445 }),
446 };
447 assert_eq!(dp.id(), 42);
448 assert_eq!(dp.external_id(), Some("ts1"));
449 assert!(!dp.is_string());
450 assert_eq!(dp.unit(), Some("°C"));
451 if let EitherDataPoint::Numeric(n) = dp.datapoint() {
452 assert_eq!(n.value, Some(23.5));
453 } else {
454 panic!("Expected numeric datapoint");
455 }
456 }
457 #[test]
458 fn test_flat_iter_stream() {
459 use futures::stream;
460 use futures::StreamExt;
461 let s = stream::iter(vec![
462 Ok::<_, crate::Error>(vec![1, 2, 3]),
463 Ok(vec![4, 5]),
464 Ok(vec![6]),
465 ]);
466 let mut flat_stream = FlatIterStream::new(s);
467 let mut results = Vec::new();
468 while let Some(item) = futures::executor::block_on(flat_stream.next()) {
469 results.push(item.unwrap());
470 }
471 assert_eq!(results, vec![1, 2, 3, 4, 5, 6]);
472 }
473}