1use anyhow::{Context, Result};
2use async_trait::async_trait;
3use chrono::{DateTime, DurationRound};
4use chrono::{TimeDelta, Utc};
5use micromegas_analytics::audience::audience_column_mismatch;
6use micromegas_analytics::delete::delete_old_data;
7use micromegas_analytics::lakehouse::batch_update::materialize_partition_range;
8use micromegas_analytics::lakehouse::lakehouse_context::LakehouseContext;
9use micromegas_analytics::lakehouse::partition_cache::PartitionCache;
10use micromegas_analytics::lakehouse::temp::delete_expired_temporary_files;
11use micromegas_analytics::lakehouse::view::View;
12use micromegas_analytics::lakehouse::view_factory::ViewFactory;
13use micromegas_analytics::lakehouse::view_registry::ViewRegistry;
14use micromegas_analytics::response_writer::ResponseWriter;
15use micromegas_analytics::time::TimeRange;
16use micromegas_tracing::prelude::*;
17use std::future::Future;
18use std::sync::Arc;
19use std::time::Duration;
20use tokio::task::{JoinError, JoinSet};
21
22use super::cron_task::{CronTask, TaskCallback};
23use super::pg_stats::PgStatsTask;
24
25fn sorted_views_from_registry(registry: &ViewRegistry) -> Arc<Vec<Arc<dyn View>>> {
30 let mut views = get_global_views_with_update_group(®istry.current());
31 views.sort_by_key(|v| v.get_update_group().unwrap_or(i32::MAX));
32 Arc::new(views)
33}
34
35#[span_fn]
52pub async fn materialize_all_views(
53 lakehouse: Arc<LakehouseContext>,
54 views: Arc<Vec<Arc<dyn View>>>,
55 insert_range: TimeRange,
56 partition_time_delta: TimeDelta,
57) -> Result<()> {
58 if views.is_empty() {
61 return Ok(());
62 }
63 let mut last_group = views.first().unwrap().get_update_group();
64 let mut partitions_all_views = Arc::new(
65 PartitionCache::fetch_overlapping_insert_range(&lakehouse.lake().db_pool, insert_range)
66 .await?,
67 );
68 let null_response_writer = Arc::new(ResponseWriter::new(None));
69 let mut failures = Vec::new();
70 for view in &*views {
71 if view.get_update_group() != last_group {
72 last_group = view.get_update_group();
79 partitions_all_views = Arc::new(
80 PartitionCache::fetch_overlapping_insert_range(
81 &lakehouse.lake().db_pool,
83 insert_range,
84 )
85 .await?,
86 );
87 }
88 let view_set_name = view.get_view_set_name();
89 let view_instance_id = view.get_view_instance_id();
90 if let Err(e) = materialize_partition_range(
91 partitions_all_views.clone(),
92 lakehouse.clone(),
93 view.clone(),
94 insert_range,
95 partition_time_delta,
96 null_response_writer.clone(),
97 )
98 .await
99 {
100 error!("materialize_all_views: {view_set_name} {view_instance_id} failed: {e:?}");
101 imetric!(
102 "materialize_view_failure",
103 "count",
104 view_set_name = view_set_name,
105 view_instance_id = view_instance_id,
106 1
107 );
108 failures.push(format!("{view_set_name} {view_instance_id}"));
109 }
110 }
111 if !failures.is_empty() {
112 anyhow::bail!(
113 "materialize_all_views: {} view(s) failed:\n{}",
114 failures.len(),
115 failures.join("\n")
116 );
117 }
118 Ok(())
119}
120
121pub struct EveryDayTask {
123 pub lakehouse: Arc<LakehouseContext>,
124 pub view_registry: Arc<ViewRegistry>,
125}
126
127#[async_trait]
128impl TaskCallback for EveryDayTask {
129 #[span_fn]
130 async fn run(&self, task_scheduled_time: DateTime<Utc>) -> Result<()> {
131 let partition_time_delta = TimeDelta::days(1);
132 let trunc_task_time = task_scheduled_time.duration_trunc(partition_time_delta)?;
133 let begin_range = trunc_task_time - (partition_time_delta * 2);
134 let end_range = trunc_task_time;
135 let views = sorted_views_from_registry(&self.view_registry);
136 materialize_all_views(
137 self.lakehouse.clone(),
138 views,
139 TimeRange::new(begin_range, end_range),
140 partition_time_delta,
141 )
142 .await
143 }
144}
145
146async fn measure_block_stream_process_id_mismatch(
153 pool: &sqlx::PgPool,
154 since: DateTime<Utc>,
155) -> Result<()> {
156 let count: i64 = sqlx::query_scalar(
157 "SELECT count(*) FROM blocks b
158 JOIN streams s ON s.stream_id = b.stream_id
159 WHERE b.process_id <> s.process_id
160 AND b.insert_time >= $1",
161 )
162 .bind(since)
163 .fetch_one(pool)
164 .await
165 .with_context(|| "counting block_stream_process_id_mismatch")?;
166 if count > 0 {
167 warn!(
168 "block_stream_process_id_mismatch: {count} block(s) in the last hour disagree with their stream's process_id"
169 );
170 }
171 imetric!("block_stream_process_id_mismatch", "count", count as u64);
172 Ok(())
173}
174
175async fn measure_block_audience_mismatch_rows(
184 pool: &sqlx::PgPool,
185 since: DateTime<Utc>,
186) -> Result<()> {
187 let predicate = format!(
188 "({} OR {})",
189 audience_column_mismatch("b", "s"),
190 audience_column_mismatch("b", "p"),
191 );
192 let sql = format!(
193 "SELECT count(*) FROM blocks b
194 JOIN streams s ON s.stream_id = b.stream_id
195 JOIN processes p ON p.process_id = b.process_id
196 WHERE {predicate}
197 AND b.insert_time >= $1"
198 );
199 let count: i64 = sqlx::query_scalar(&sql)
200 .bind(since)
201 .fetch_one(pool)
202 .await
203 .with_context(|| "counting block_audience_mismatch_rows")?;
204 if count > 0 {
205 warn!(
206 "block_audience_mismatch_rows: {count} block(s) in the last hour disagree with their \
207 stream's or process's audience -- silently dropped from blocks/log_entries/measures/log_stats \
208 by the audience-mismatch predicate"
209 );
210 }
211 imetric!("block_audience_mismatch_rows", "count", count as u64);
212 Ok(())
213}
214
215pub struct EveryHourTask {
217 pub lakehouse: Arc<LakehouseContext>,
218 pub view_registry: Arc<ViewRegistry>,
219 pub retention_days: i32,
220}
221
222#[async_trait]
223impl TaskCallback for EveryHourTask {
224 #[span_fn]
225 async fn run(&self, task_scheduled_time: DateTime<Utc>) -> Result<()> {
226 delete_old_data(self.lakehouse.lake(), self.retention_days).await?;
227 delete_expired_temporary_files(self.lakehouse.lake().clone()).await?;
228
229 let since = task_scheduled_time - TimeDelta::hours(1);
230 measure_block_stream_process_id_mismatch(&self.lakehouse.lake().db_pool, since).await?;
231 measure_block_audience_mismatch_rows(&self.lakehouse.lake().db_pool, since).await?;
232
233 let partition_time_delta = TimeDelta::hours(1);
234 let trunc_task_time = task_scheduled_time.duration_trunc(partition_time_delta)?;
235 let begin_range = trunc_task_time - (partition_time_delta * 2);
236 let end_range = trunc_task_time;
237 let views = sorted_views_from_registry(&self.view_registry);
238 materialize_all_views(
239 self.lakehouse.clone(),
240 views,
241 TimeRange::new(begin_range, end_range),
242 partition_time_delta,
243 )
244 .await
245 }
246}
247
248pub struct EveryMinuteTask {
250 pub lakehouse: Arc<LakehouseContext>,
251 pub view_registry: Arc<ViewRegistry>,
252}
253
254#[async_trait]
255impl TaskCallback for EveryMinuteTask {
256 #[span_fn]
257 async fn run(&self, task_scheduled_time: DateTime<Utc>) -> Result<()> {
258 let partition_time_delta = TimeDelta::minutes(1);
259 let trunc_task_time = task_scheduled_time.duration_trunc(partition_time_delta)?;
260 let begin_range = trunc_task_time - (partition_time_delta * 2);
261 let end_range = trunc_task_time - partition_time_delta;
263 let views = sorted_views_from_registry(&self.view_registry);
264 materialize_all_views(
265 self.lakehouse.clone(),
266 views,
267 TimeRange::new(begin_range, end_range),
268 partition_time_delta,
269 )
270 .await
271 }
272}
273
274pub struct EverySecondTask {
276 pub lakehouse: Arc<LakehouseContext>,
277 pub view_registry: Arc<ViewRegistry>,
278}
279
280#[async_trait]
281impl TaskCallback for EverySecondTask {
282 #[span_fn]
283 async fn run(&self, task_scheduled_time: DateTime<Utc>) -> Result<()> {
284 let delay = Utc::now() - task_scheduled_time;
285 if delay > TimeDelta::seconds(10) {
286 warn!("skipping `seconds` task, delay={delay}");
288 return Ok(());
289 }
290 let partition_time_delta = TimeDelta::seconds(1);
291 let trunc_task_time = task_scheduled_time.duration_trunc(partition_time_delta)?;
292 let begin_range = trunc_task_time - (partition_time_delta * 2);
293 let end_range = trunc_task_time - partition_time_delta;
295 let views = sorted_views_from_registry(&self.view_registry);
296 materialize_all_views(
297 self.lakehouse.clone(),
298 views,
299 TimeRange::new(begin_range, end_range),
300 partition_time_delta,
301 )
302 .await
303 }
304}
305
306fn log_task_result(res: Result<Result<Result<()>, JoinError>, JoinError>) {
313 match res {
314 Ok(Ok(Ok(()))) => {}
315 Ok(Ok(Err(e))) => error!("{e:?}"),
316 Ok(Err(e)) => error!("{e:?}"),
317 Err(e) => error!("{e:?}"),
318 }
319}
320
321async fn drain_task_set(task_set: &mut JoinSet<Result<Result<()>, JoinError>>) {
326 while let Some(res) = task_set.join_next().await {
327 log_task_result(res);
328 }
329}
330
331pub async fn run_tasks_forever<F>(mut tasks: Vec<CronTask>, max_parallelism: usize, shutdown: F)
336where
337 F: Future<Output = ()>,
338{
339 tokio::pin!(shutdown);
340 let mut task_set = JoinSet::new();
341 loop {
342 let mut next_task_run = Utc::now() + TimeDelta::days(2);
343 for task in &mut tasks {
344 if task.get_next_run() < Utc::now() {
345 task_set.spawn(task.spawn().await);
346 if task_set.len() >= max_parallelism {
347 tokio::select! {
348 res = task_set.join_next() => {
349 if let Some(res) = res {
350 log_task_result(res);
351 }
352 }
353 _ = &mut shutdown => {
354 drain_task_set(&mut task_set).await;
355 return;
356 }
357 }
358 }
359 }
360 let task_next_run = task.get_next_run();
361 if task_next_run < next_task_run {
362 next_task_run = task_next_run;
363 }
364 }
365 let time_until_next_task = next_task_run - Utc::now();
366 if time_until_next_task > TimeDelta::zero() {
367 match time_until_next_task
368 .to_std()
369 .with_context(|| "delay.to_std")
370 {
371 Ok(wait) => {
372 tokio::select! {
373 _ = tokio::time::sleep(wait) => {}
374 _ = &mut shutdown => {
375 drain_task_set(&mut task_set).await;
376 return;
377 }
378 }
379 }
380 Err(e) => warn!("{e:?}"),
381 }
382 } else {
383 tokio::select! {
386 biased;
387 _ = &mut shutdown => {
388 drain_task_set(&mut task_set).await;
389 return;
390 }
391 _ = tokio::task::yield_now() => {}
392 }
393 }
394 }
395}
396
397pub fn get_global_views_with_update_group(view_factory: &ViewFactory) -> Vec<Arc<dyn View>> {
402 view_factory
403 .get_global_views()
404 .iter()
405 .filter(|v| v.get_update_group().is_some())
406 .cloned()
407 .collect()
408}
409
410pub async fn daemon<F>(
430 lakehouse: Arc<LakehouseContext>,
431 view_registry: Arc<ViewRegistry>,
432 retention_days: i32,
433 shutdown: F,
434 grace: Duration,
435) -> Result<()>
436where
437 F: Future<Output = ()> + Send + 'static,
438{
439 use super::shutdown::ShutdownFanout;
440
441 view_registry
445 .reload()
446 .await
447 .with_context(|| "initial view_registry reload")?;
448
449 let every_day = CronTask::new(
450 String::from("every_day"),
451 TimeDelta::days(1),
452 TimeDelta::hours(4),
453 Arc::new(EveryDayTask {
454 lakehouse: lakehouse.clone(),
455 view_registry: view_registry.clone(),
456 }),
457 )?;
458 let every_hour = CronTask::new(
459 String::from("every_hour"),
460 TimeDelta::hours(1),
461 TimeDelta::minutes(10),
462 Arc::new(EveryHourTask {
463 lakehouse: lakehouse.clone(),
464 view_registry: view_registry.clone(),
465 retention_days,
466 }),
467 )?;
468 let every_minute = CronTask::new(
469 String::from("every minute"),
470 TimeDelta::minutes(1),
471 TimeDelta::seconds(30),
472 Arc::new(EveryMinuteTask {
473 lakehouse: lakehouse.clone(),
474 view_registry: view_registry.clone(),
475 }),
476 )?;
477 let pg_stats = CronTask::new(
478 String::from("pg_stats"),
479 TimeDelta::minutes(1),
480 TimeDelta::seconds(15), Arc::new(PgStatsTask {
482 lakehouse: lakehouse.clone(),
483 }),
484 )?;
485 let every_second = CronTask::new(
486 String::from("every second"),
487 TimeDelta::seconds(1),
488 TimeDelta::milliseconds(500),
489 Arc::new(EverySecondTask {
490 lakehouse,
491 view_registry: view_registry.clone(),
492 }),
493 )?;
494
495 let fanout = ShutdownFanout::new(shutdown);
496 let grace_secs = grace.as_secs();
497
498 view_registry.spawn_refresh_task(fanout.subscribe());
501
502 let mut runners = tokio::task::JoinSet::new();
503 runners.spawn(run_tasks_forever(vec![every_day], 1, fanout.subscribe()));
504 runners.spawn(run_tasks_forever(vec![every_hour], 1, fanout.subscribe()));
505 runners.spawn(run_tasks_forever(vec![every_minute], 5, fanout.subscribe()));
506 runners.spawn(run_tasks_forever(vec![every_second], 5, fanout.subscribe()));
507 runners.spawn(run_tasks_forever(vec![pg_stats], 1, fanout.subscribe()));
508
509 let deadline = {
510 let d = fanout.subscribe();
511 async move {
512 d.await;
513 tokio::time::sleep(grace).await;
514 }
515 };
516
517 tokio::select! {
518 _ = runners.join_all() => {
519 info!("daemon drain completed");
520 }
521 _ = deadline => {
522 warn!("daemon grace period of {grace_secs}s elapsed with work still in flight");
523 }
524 }
525 Ok(())
526}