1use std::time::Duration;
4
5use anyhow::Context;
6use anyhow::Result;
7use anyhow::anyhow;
8use chrono::DateTime;
9use chrono::Utc;
10use diesel::Connection;
11use diesel_async::AsyncConnection;
12use diesel_async::AsyncPgConnection;
13use diesel_async::pooled_connection::AsyncDieselConnectionManager;
14use diesel_async::pooled_connection::deadpool::Pool;
15use diesel_migrations::EmbeddedMigrations;
16use diesel_migrations::HarnessWithOutput;
17use diesel_migrations::MigrationHarness;
18use diesel_migrations::embed_migrations;
19use futures::future::BoxFuture;
20use secrecy::ExposeSecret;
21use secrecy::SecretString;
22use tes::v1::types::requests::DEFAULT_PAGE_SIZE;
23use tes::v1::types::requests::GetTaskParams;
24use tes::v1::types::requests::ListTasksParams;
25use tes::v1::types::requests::Task as RequestTask;
26use tes::v1::types::requests::View;
27use tes::v1::types::responses::ExecutorLog;
28use tes::v1::types::responses::OutputFile;
29use tes::v1::types::responses::Task;
30use tes::v1::types::responses::TaskLog;
31use tes::v1::types::responses::TaskResponse;
32use tes::v1::types::task::State;
33use tracing::debug;
34use tracing::info;
35
36use super::Database;
37use super::DatabaseResult;
38use crate::TaskTemplateData;
39use crate::TerminatedContainer;
40use crate::normalize_cpu_seconds;
41
42pub(crate) mod models;
43
44macro_rules! load_container_usage {
49 ($conn:expr, $tasks:expr) => {{
50 use diesel::*;
51 use diesel_async::RunQueryDsl;
52
53 models::ContainerUsage::belonging_to($tasks)
54 .select(models::ContainerUsage::as_select())
55 .order_by(schema::task_container_usage::container_name)
56 .load($conn)
57 .await
58 .map_err(Error::Diesel)?
59 .grouped_by($tasks)
60 }};
61}
62
63macro_rules! load_task_container_usage {
65 ($conn:expr, $task:expr) => {{
66 use diesel::*;
67 use diesel_async::RunQueryDsl;
68
69 models::ContainerUsage::belonging_to($task)
70 .select(models::ContainerUsage::as_select())
71 .order_by(schema::task_container_usage::container_name)
72 .load($conn)
73 .await
74 .map_err(Error::Diesel)?
75 }};
76}
77
78#[allow(clippy::missing_docs_in_private_items)]
79pub(crate) mod schema;
80mod usage;
81
82const MIGRATIONS: EmbeddedMigrations = embed_migrations!("src/postgres/migrations");
85
86const POOL_RETAIN_INTERVAL: Duration = Duration::from_secs(30);
89
90const MAX_CONNECTION_AGE: Duration = Duration::from_secs(60);
93
94const MAX_POOL_SIZE: usize = 10;
98
99const MEMORY_USAGE_FOLD_SQL: &str = r#"
101 INSERT INTO task_container_usage (
102 task_id,
103 container_name,
104 peak_memory_bytes,
105 memory_total_bytes,
106 memory_sample_count
107 )
108 SELECT
109 tasks.id,
110 samples.container_name,
111 MAX(samples.memory_bytes),
112 SUM(samples.memory_bytes),
113 COUNT(samples.memory_bytes)
114 FROM UNNEST($1::text[], $2::text[], $3::bigint[])
115 AS samples(tes_id, container_name, memory_bytes)
116 JOIN tasks ON tasks.tes_id = samples.tes_id
117 WHERE samples.memory_bytes IS NOT NULL
118 GROUP BY tasks.id, samples.container_name
119 ORDER BY tasks.id, samples.container_name
120 ON CONFLICT (task_id, container_name) DO UPDATE SET
121 peak_memory_bytes = GREATEST(
122 COALESCE(task_container_usage.peak_memory_bytes, 0),
123 EXCLUDED.peak_memory_bytes
124 ),
125 memory_total_bytes =
126 COALESCE(task_container_usage.memory_total_bytes, 0)
127 + EXCLUDED.memory_total_bytes,
128 memory_sample_count =
129 COALESCE(task_container_usage.memory_sample_count, 0)
130 + EXCLUDED.memory_sample_count
131"#;
132
133const CPU_USAGE_FOLD_SQL: &str = r#"
135 WITH samples AS (
136 SELECT
137 tasks.id AS task_id,
138 observations.pod_name,
139 observations.container_name,
140 observations.cpu_seconds,
141 observations.start_time_seconds
142 FROM UNNEST($1::text[], $2::text[], $3::text[], $4::float8[], $5::float8[])
143 AS observations(
144 tes_id,
145 pod_name,
146 container_name,
147 cpu_seconds,
148 start_time_seconds
149 )
150 JOIN tasks ON tasks.tes_id = observations.tes_id
151 WHERE observations.cpu_seconds IS NOT NULL
152 ),
153 deltas AS (
154 SELECT
155 samples.*,
156 CASE
157 WHEN baseline.cpu_seconds IS NULL THEN samples.cpu_seconds
158 WHEN baseline.start_time_seconds
159 IS NOT DISTINCT FROM samples.start_time_seconds
160 AND samples.cpu_seconds >= baseline.cpu_seconds
161 THEN samples.cpu_seconds - baseline.cpu_seconds
162 ELSE samples.cpu_seconds
163 END AS cpu_delta_seconds
164 FROM samples
165 LEFT JOIN task_container_baseline AS baseline
166 USING (task_id, pod_name, container_name)
167 ),
168 -- Baseline advancement and aggregation must stay atomic for retry idempotence.
169 advanced AS (
170 INSERT INTO task_container_baseline (
171 task_id,
172 pod_name,
173 container_name,
174 start_time_seconds,
175 cpu_seconds
176 )
177 SELECT
178 task_id,
179 pod_name,
180 container_name,
181 start_time_seconds,
182 cpu_seconds
183 FROM deltas
184 ON CONFLICT (task_id, pod_name, container_name) DO UPDATE SET
185 start_time_seconds = EXCLUDED.start_time_seconds,
186 cpu_seconds = EXCLUDED.cpu_seconds
187 )
188 INSERT INTO task_container_usage (
189 task_id,
190 container_name,
191 cpu_seconds
192 )
193 SELECT
194 task_id,
195 container_name,
196 SUM(cpu_delta_seconds)
197 FROM deltas
198 -- Multiple pods can map to one row, so group before ON CONFLICT.
199 GROUP BY task_id, container_name
200 ORDER BY task_id, container_name
201 ON CONFLICT (task_id, container_name) DO UPDATE SET
202 cpu_seconds =
203 COALESCE(task_container_usage.cpu_seconds, 0.0)
204 + EXCLUDED.cpu_seconds
205"#;
206
207fn zip_longest<A, B>(a: A, b: B) -> impl Iterator<Item = (A::Item, B::Item)>
211where
212 A: IntoIterator,
213 A::Item: Default,
214 B: IntoIterator,
215 B::Item: Default,
216{
217 let mut a = a.into_iter();
218 let mut b = b.into_iter();
219 std::iter::from_fn(move || match (a.next(), b.next()) {
220 (None, None) => None,
221 (a, b) => Some((a.unwrap_or_default(), b.unwrap_or_default())),
222 })
223}
224
225pub fn format_database_url(
227 user: &str,
228 password: &SecretString,
229 host: &str,
230 port: i32,
231 database_name: &str,
232 app_name: &str,
233) -> String {
234 format!(
235 "postgres://{user}:{password}@{host}:{port}/{database_name}?application_name={app_name}",
236 password = password.expose_secret(),
237 )
238}
239
240#[derive(Debug, thiserror::Error)]
242pub enum Error {
243 #[error("task `{0}` was not found")]
245 TaskNotFound(String),
246 #[error(transparent)]
248 Pool(#[from] diesel_async::pooled_connection::deadpool::PoolError),
249 #[error(transparent)]
251 Diesel(#[from] diesel::result::Error),
252}
253
254fn into_task<T, C>(task: T, containers: Vec<C>, usage: Vec<models::ContainerUsage>) -> Task
256where
257 T: Into<(Task, Vec<OutputFile>, Vec<String>)>,
258 C: Into<ExecutorLog>,
259{
260 let (mut task, outputs, system_logs) = task.into();
261 let metadata = usage::resource_usage_metadata(&usage);
262 let executor_logs: Vec<_> = containers.into_iter().map(Into::into).collect();
263
264 if !outputs.is_empty()
265 || !executor_logs.is_empty()
266 || !system_logs.is_empty()
267 || metadata.is_some()
268 {
269 let start_time = executor_logs.first().and_then(|e| e.start_time);
270 let end_time = executor_logs.last().and_then(|e| e.end_time);
271
272 task.logs = Some(vec![TaskLog {
273 logs: executor_logs,
274 metadata,
275 start_time,
276 end_time,
277 outputs,
278 system_logs: if system_logs.is_empty() {
279 None
280 } else {
281 Some(system_logs)
282 },
283 }]);
284 }
285
286 task
287}
288
289pub struct PostgresDatabase {
291 url: SecretString,
293 pool: Pool<AsyncPgConnection>,
295}
296
297impl PostgresDatabase {
298 pub fn new(url: SecretString) -> Result<Self> {
300 let config = AsyncDieselConnectionManager::new(url.expose_secret());
301 debug!("creating database connection pool with {MAX_POOL_SIZE} slots");
302
303 let pool = Pool::builder(config)
304 .max_size(MAX_POOL_SIZE)
305 .build()
306 .context("failed to initialize PostgreSQL connection pool")?;
307
308 let p = pool.clone();
309
310 tokio::spawn(async move {
313 loop {
314 tokio::time::sleep(POOL_RETAIN_INTERVAL).await;
315
316 let res = p.retain(|_, metrics| metrics.last_used() < MAX_CONNECTION_AGE);
317
318 debug!(
319 "removed {removed} and retained {retained} connections(s) from the database \
320 connection pool",
321 removed = res.removed.len(),
322 retained = res.retained
323 );
324 }
325 });
326
327 Ok(Self { url, pool })
328 }
329
330 pub async fn run_pending_migrations(&self) -> Result<()> {
332 struct Writer;
333 impl std::io::Write for Writer {
334 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
335 let buf = String::from_utf8_lossy(buf);
336 info!("{buf}", buf = buf.trim_end());
337 Ok(buf.len())
338 }
339
340 fn flush(&mut self) -> std::io::Result<()> {
341 Ok(())
342 }
343 }
344
345 let mut conn = diesel::pg::PgConnection::establish(self.url.expose_secret())?;
348 HarnessWithOutput::new(&mut conn, std::io::LineWriter::new(Writer))
349 .run_pending_migrations(MIGRATIONS)
350 .map_err(|e| anyhow!("failed to run pending database migrations: {e}"))?;
351
352 Ok(())
353 }
354}
355
356#[async_trait::async_trait]
357impl Database for PostgresDatabase {
358 async fn insert_task(&self, username: &str, task: &RequestTask) -> DatabaseResult<String> {
359 use diesel_async::RunQueryDsl;
360
361 let task = models::NewTask::new(username, task);
362
363 let mut conn = self.pool.get().await.map_err(Error::Pool)?;
365 diesel::insert_into(schema::tasks::table)
366 .values(&task)
367 .execute(&mut conn)
368 .await
369 .map_err(Error::Diesel)?;
370
371 Ok(task.tes_id)
372 }
373
374 async fn get_task(
375 &self,
376 username: &str,
377 tes_id: &str,
378 params: GetTaskParams,
379 ) -> DatabaseResult<TaskResponse> {
380 use diesel::*;
381 use diesel_async::RunQueryDsl;
382
383 let mut conn = self.pool.get().await.map_err(Error::Pool)?;
384
385 match params.view {
386 View::Minimal => Ok(TaskResponse::Minimal(
387 schema::tasks::table
388 .select(models::MinimalTask::as_select())
389 .filter(
390 schema::tasks::username
391 .eq(username)
392 .and(schema::tasks::tes_id.eq(tes_id)),
393 )
394 .first(&mut conn)
395 .await
396 .optional()
397 .map_err(Error::Diesel)?
398 .ok_or_else(|| Error::TaskNotFound(tes_id.to_string()))?
399 .into(),
400 )),
401 View::Basic => {
402 let task = schema::tasks::table
403 .select(models::BasicTask::as_select())
404 .filter(
405 schema::tasks::username
406 .eq(username)
407 .and(schema::tasks::tes_id.eq(tes_id)),
408 )
409 .first(&mut conn)
410 .await
411 .optional()
412 .map_err(Error::Diesel)?
413 .ok_or_else(|| Error::TaskNotFound(tes_id.to_string()))?;
414
415 let containers = models::BasicContainer::belonging_to(&task)
416 .select(models::BasicContainer::as_select())
417 .filter(schema::containers::executor_index.is_not_null())
418 .order_by(schema::containers::executor_index)
419 .load(&mut conn)
420 .await
421 .map_err(Error::Diesel)?;
422
423 let usage = load_task_container_usage!(&mut conn, &task);
424
425 Ok(TaskResponse::Basic(into_task(task, containers, usage)))
426 }
427 View::Full => {
428 let task = schema::tasks::table
429 .select(models::FullTask::as_select())
430 .filter(
431 schema::tasks::username
432 .eq(username)
433 .and(schema::tasks::tes_id.eq(tes_id)),
434 )
435 .first(&mut conn)
436 .await
437 .optional()
438 .map_err(Error::Diesel)?
439 .ok_or_else(|| Error::TaskNotFound(tes_id.to_string()))?;
440
441 let containers = models::FullContainer::belonging_to(&task)
442 .select(models::FullContainer::as_select())
443 .filter(schema::containers::executor_index.is_not_null())
444 .order_by(schema::containers::executor_index)
445 .load(&mut conn)
446 .await
447 .map_err(Error::Diesel)?;
448
449 let usage = load_task_container_usage!(&mut conn, &task);
450
451 Ok(TaskResponse::Full(into_task(task, containers, usage)))
452 }
453 }
454 }
455
456 async fn get_task_template_data(&self, tes_id: &str) -> DatabaseResult<TaskTemplateData> {
457 use diesel::*;
458 use diesel_async::RunQueryDsl;
459
460 let mut conn = self.pool.get().await.map_err(Error::Pool)?;
461 Ok(schema::tasks::table
462 .select(models::TaskTemplateData::as_select())
463 .filter(schema::tasks::tes_id.eq(tes_id))
464 .first(&mut conn)
465 .await
466 .optional()
467 .map_err(Error::Diesel)?
468 .ok_or_else(|| Error::TaskNotFound(tes_id.to_string()))?
469 .into())
470 }
471
472 async fn get_tasks(
473 &self,
474 username: &str,
475 params: ListTasksParams,
476 ) -> DatabaseResult<(Vec<TaskResponse>, Option<String>)> {
477 use diesel::*;
478 use diesel_async::RunQueryDsl;
479
480 let mut query = schema::tasks::table.into_boxed();
481
482 query = query.filter(schema::tasks::username.eq(username));
483
484 if let Some(prefix) = ¶ms.name_prefix {
486 query = query.filter(schema::tasks::name.like(format!("{prefix}%")));
487 }
488
489 if let Some(state) = params.state {
491 query = query.filter(schema::tasks::state.eq(models::TaskState::from(state)));
492 }
493
494 let offset = if let Some(page_token) = params.page_token {
496 let offset: i64 = page_token
497 .parse()
498 .map_err(|_| super::Error::InvalidPageToken(page_token.clone()))?;
499
500 if offset < 0 {
501 return Err(super::Error::InvalidPageToken(page_token));
502 }
503
504 query = query.offset(offset);
505 offset
506 } else {
507 0
508 };
509
510 for (k, v) in zip_longest(
512 params.tag_keys.unwrap_or_default(),
513 params.tag_values.unwrap_or_default(),
514 ) {
515 if !v.is_empty() {
516 query = query.filter(
517 schema::tasks::tags.contains(models::Json(models::TagFilter::new(k, v))),
518 );
519 } else {
520 query = query.filter(schema::tasks::tags.has_key(k));
521 }
522 }
523
524 let page_size = params.page_size.unwrap_or(DEFAULT_PAGE_SIZE);
526 query = query.limit(page_size as i64).order_by(schema::tasks::id);
527
528 let mut conn = self.pool.get().await.map_err(Error::Pool)?;
529
530 match params.view.unwrap_or_default() {
531 View::Minimal => {
532 let tasks = query
533 .select(models::MinimalTask::as_select())
534 .load(&mut conn)
535 .await
536 .map_err(Error::Diesel)?;
537
538 let token = if tasks.len() < page_size as usize {
539 None
540 } else {
541 Some((offset as usize + tasks.len()).to_string())
542 };
543
544 Ok((
545 tasks
546 .into_iter()
547 .map(|t| TaskResponse::Minimal(t.into()))
548 .collect(),
549 token,
550 ))
551 }
552 View::Basic => {
553 let tasks: Vec<_> = query
554 .select(models::BasicTask::as_select())
555 .load(&mut conn)
556 .await
557 .map_err(Error::Diesel)?
558 .into_iter()
559 .collect();
560
561 let token = if tasks.len() < page_size as usize {
562 None
563 } else {
564 Some((offset as usize + tasks.len()).to_string())
565 };
566
567 let usage = load_container_usage!(&mut conn, &tasks);
568
569 Ok((
570 models::BasicContainer::belonging_to(&tasks)
571 .select(models::BasicContainer::as_select())
572 .filter(schema::containers::executor_index.is_not_null())
573 .order_by(schema::containers::executor_index)
574 .load(&mut conn)
575 .await
576 .map_err(Error::Diesel)?
577 .grouped_by(&tasks)
578 .into_iter()
579 .zip(usage)
580 .zip(tasks)
581 .map(|((containers, usage), task)| {
582 TaskResponse::Basic(into_task(task, containers, usage))
583 })
584 .collect(),
585 token,
586 ))
587 }
588 View::Full => {
589 let tasks: Vec<_> = query
590 .select(models::FullTask::as_select())
591 .load(&mut conn)
592 .await
593 .map_err(Error::Diesel)?
594 .into_iter()
595 .collect();
596
597 let token = if tasks.len() < page_size as usize {
598 None
599 } else {
600 Some((offset as usize + tasks.len()).to_string())
601 };
602
603 let usage = load_container_usage!(&mut conn, &tasks);
604
605 Ok((
606 models::FullContainer::belonging_to(&tasks)
607 .select(models::FullContainer::as_select())
608 .filter(schema::containers::executor_index.is_not_null())
609 .order_by(schema::containers::executor_index)
610 .load(&mut conn)
611 .await
612 .map_err(Error::Diesel)?
613 .grouped_by(&tasks)
614 .into_iter()
615 .zip(usage)
616 .zip(tasks)
617 .map(|((containers, usage), task)| {
618 TaskResponse::Full(into_task(task, containers, usage))
619 })
620 .collect(),
621 token,
622 ))
623 }
624 }
625 }
626
627 async fn get_in_progress_tasks(&self, before: DateTime<Utc>) -> DatabaseResult<Vec<String>> {
628 use diesel::pg::sql_types::Timestamptz;
629 use diesel::*;
630 use diesel_async::RunQueryDsl;
631 use models::TaskState;
632
633 let mut conn = self.pool.get().await.map_err(Error::Pool)?;
634
635 Ok(schema::tasks::table
636 .select(schema::tasks::tes_id)
637 .filter(
638 schema::tasks::state
639 .eq_any(&[
640 TaskState::Unknown,
641 TaskState::Queued,
642 TaskState::Initializing,
643 TaskState::Running,
644 ])
645 .and(schema::tasks::creation_time.le(before.into_sql::<Timestamptz>())),
646 )
647 .get_results(&mut conn)
648 .await
649 .map_err(Error::Diesel)?)
650 }
651
652 async fn update_task_state<'a>(
653 &self,
654 tes_id: &str,
655 state: State,
656 messages: &[&str],
657 containers: Option<BoxFuture<'a, Result<Vec<TerminatedContainer<'a>>>>>,
658 outputs: Option<&[OutputFile]>,
659 ) -> DatabaseResult<bool> {
660 use diesel::pg::sql_types::Array;
661 use diesel::sql_types::Text;
662 use diesel::*;
663 use diesel_async::RunQueryDsl;
664 use models::TaskState;
665
666 #[derive(QueryableByName)]
670 #[diesel(table_name = schema::tasks)]
671 #[diesel(check_for_backend(diesel::pg::Pg))]
672 struct UpdatedTask {
673 id: i32,
675 }
676
677 let previous: &[TaskState] = match state {
679 State::Unknown | State::Paused => {
681 return Ok(false);
682 }
683 State::Queued => &[TaskState::Unknown],
685 State::Initializing => &[TaskState::Unknown, TaskState::Queued],
687 State::Running => &[
689 TaskState::Unknown,
690 TaskState::Queued,
691 TaskState::Initializing,
692 ],
693 State::Complete | State::ExecutorError => &[
695 TaskState::Unknown,
696 TaskState::Queued,
697 TaskState::Initializing,
698 TaskState::Running,
699 ],
700 State::SystemError | State::Canceling => &[
702 TaskState::Unknown,
703 TaskState::Queued,
704 TaskState::Initializing,
705 TaskState::Running,
706 ],
707 State::Canceled => &[TaskState::Canceling],
709 State::Preempted => &[
711 TaskState::Unknown,
712 TaskState::Queued,
713 TaskState::Initializing,
714 TaskState::Running,
715 ],
716 };
717
718 let mut conn = self.pool.get().await.map_err(Error::Pool)?;
719
720 let updated = conn
721 .transaction(async |conn| {
722 let updated: Option<UpdatedTask> = sql_query(
726 "UPDATE tasks SET state = $1, system_logs = array_cat(system_logs, $2) WHERE \
727 tes_id = $3 AND state = ANY ($4) RETURNING id",
728 )
729 .bind::<schema::sql_types::TaskState, _>(TaskState::from(state))
730 .bind::<Array<Text>, _>(messages)
731 .bind::<Text, _>(tes_id)
732 .bind::<Array<schema::sql_types::TaskState>, _>(previous)
733 .get_result(conn)
734 .await
735 .optional()
736 .map_err(Error::Diesel)?;
737
738 match updated {
739 Some(UpdatedTask { id }) => {
740 if let Some(outputs) = outputs {
741 diesel::update(schema::tasks::table)
742 .filter(
743 schema::tasks::tes_id
744 .eq(tes_id)
745 .and(schema::tasks::output_files.is_null()),
746 )
747 .set(schema::tasks::output_files.eq(models::Json(outputs)))
748 .execute(conn)
749 .await
750 .map_err(Error::Diesel)?;
751 }
752
753 if let Some(containers) = containers {
754 let containers = containers.await?;
756 diesel::insert_into(schema::containers::table)
757 .values(
758 containers
759 .into_iter()
760 .map(|c| models::NewContainer::new(id, c))
761 .collect::<Vec<_>>(),
762 )
763 .on_conflict_do_nothing()
764 .execute(conn)
765 .await
766 .map_err(Error::Diesel)?;
767 }
768
769 anyhow::Ok(true)
770 }
771 None => Ok(false),
772 }
773 })
774 .await?;
775
776 Ok(updated)
777 }
778
779 async fn append_system_log(&self, tes_id: &str, messages: &[&str]) -> DatabaseResult<()> {
780 use diesel::pg::sql_types::Array;
781 use diesel::sql_types::Text;
782 use diesel::*;
783 use diesel_async::RunQueryDsl;
784
785 let mut conn = self.pool.get().await.map_err(Error::Pool)?;
786
787 sql_query("UPDATE tasks SET system_logs = array_cat(system_logs, $1) WHERE tes_id = $2")
791 .bind::<Array<Text>, _>(messages)
792 .bind::<Text, _>(tes_id)
793 .execute(&mut conn)
794 .await
795 .map_err(Error::Diesel)?;
796
797 Ok(())
798 }
799
800 async fn add_task_resource_usage_samples(
801 &self,
802 samples: &[crate::ContainerUsageSample],
803 ) -> DatabaseResult<()> {
804 use diesel::pg::sql_types::Array;
805 use diesel::sql_types::BigInt;
806 use diesel::sql_types::Double;
807 use diesel::sql_types::Nullable;
808 use diesel::sql_types::Text;
809 use diesel::*;
810 use diesel_async::RunQueryDsl;
811
812 if samples.is_empty() {
813 return Ok(());
814 }
815
816 let mut ids = Vec::with_capacity(samples.len());
817 let mut pods = Vec::with_capacity(samples.len());
818 let mut names = Vec::with_capacity(samples.len());
819 let mut memory = Vec::with_capacity(samples.len());
820 let mut cpu = Vec::with_capacity(samples.len());
821 let mut start = Vec::with_capacity(samples.len());
822 for sample in samples {
823 ids.push(sample.tes_id.as_str());
824 pods.push(sample.pod_name.as_str());
825 names.push(sample.container_name.as_str());
826 memory.push(sample.memory_bytes);
827 cpu.push(sample.cpu_seconds.and_then(normalize_cpu_seconds));
828 start.push(sample.start_time_seconds);
829 }
830
831 let mut conn = self.pool.get().await.map_err(Error::Pool)?;
832
833 conn.transaction(async |conn| {
836 sql_query(MEMORY_USAGE_FOLD_SQL)
837 .bind::<Array<Text>, _>(&ids)
838 .bind::<Array<Text>, _>(&names)
839 .bind::<Array<Nullable<BigInt>>, _>(&memory)
840 .execute(conn)
841 .await
842 .map_err(Error::Diesel)?;
843
844 sql_query(CPU_USAGE_FOLD_SQL)
845 .bind::<Array<Text>, _>(&ids)
846 .bind::<Array<Text>, _>(&pods)
847 .bind::<Array<Text>, _>(&names)
848 .bind::<Array<Nullable<Double>>, _>(&cpu)
849 .bind::<Array<Nullable<Double>>, _>(&start)
850 .execute(conn)
851 .await
852 .map_err(Error::Diesel)?;
853
854 Ok::<(), Error>(())
855 })
856 .await?;
857
858 Ok(())
859 }
860
861 async fn insert_error(
862 &self,
863 source: &str,
864 tes_id: Option<&str>,
865 message: &str,
866 ) -> DatabaseResult<()> {
867 use diesel::*;
868 use diesel_async::RunQueryDsl;
869
870 let mut conn = self.pool.get().await.map_err(Error::Pool)?;
871
872 conn.transaction(async |conn| {
873 let task_id = if let Some(tes_id) = tes_id {
875 Some(
876 schema::tasks::table
877 .select(schema::tasks::id)
878 .filter(schema::tasks::tes_id.eq(tes_id))
879 .for_update()
880 .first(conn)
881 .await
882 .optional()
883 .map_err(Error::Diesel)?
884 .ok_or_else(|| Error::TaskNotFound(tes_id.to_string()))?,
885 )
886 } else {
887 None
888 };
889
890 diesel::insert_into(schema::errors::table)
892 .values(models::NewError {
893 source,
894 task_id,
895 message,
896 })
897 .execute(conn)
898 .await
899 .map_err(Error::Diesel)
900 })
901 .await?;
902
903 Ok(())
904 }
905}