1use std::sync::Arc;
2use std::sync::atomic::{AtomicU64, Ordering};
3
4use sofie_core::{Date, Error, Result, SqlValue, Timestamp};
5use sofie_telemetry::{ProductRecord, keep_product_events};
6use tokio::sync::{mpsc, oneshot};
7
8use crate::{Db, Dialect, query_decoded, query_values};
9
10const QUEUED: usize = 8192;
12const BATCH: usize = 512;
13
14pub struct ProductSql {
16 pub insert: &'static str,
17 pub cohorts: &'static str,
18 pub prune: &'static str,
20 pub forget: &'static str,
21 pub sweep: &'static str,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum Period {
27 Day,
28 Week,
29 Month,
30}
31
32impl Period {
33 #[must_use]
34 pub fn parse(text: &str) -> Option<Self> {
35 match text {
36 "day" => Some(Self::Day),
37 "week" => Some(Self::Week),
38 "month" => Some(Self::Month),
39 _ => None,
40 }
41 }
42
43 #[must_use]
44 pub fn name(self) -> &'static str {
45 match self {
46 Self::Day => "day",
47 Self::Week => "week",
48 Self::Month => "month",
49 }
50 }
51}
52
53pub struct Retention<'a> {
55 pub start: &'a str,
56 pub returning: &'a str,
57 pub period: Period,
58 pub since: Timestamp,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct Cohort {
63 pub start: Date,
64 pub size: u64,
65 pub returned: Vec<u64>,
66}
67
68pub async fn cohorts(db: &Db, retention: &Retention<'_>) -> Result<Vec<Cohort>> {
70 let sql = product_sql(db.driver().dialect())?;
71 let params = [
72 SqlValue::Text(retention.period.name().to_owned()),
73 SqlValue::Text(retention.start.to_owned()),
74 SqlValue::Text(retention.returning.to_owned()),
75 SqlValue::Timestamp(retention.since),
76 ];
77 let rows = query_values(db.driver(), sql.cohorts, ¶ms).await?;
78 let mut cohorts: Vec<Cohort> = Vec::new();
79 for row in rows {
80 let [
81 SqlValue::DateTime(start),
82 SqlValue::Int(offset),
83 SqlValue::Int(count),
84 ] = row.as_slice()
85 else {
86 return Err(Error::internal_message(format!(
87 "a cohort row has unexpected columns: {:?}",
88 row.iter().map(SqlValue::kind).collect::<Vec<_>>()
89 )));
90 };
91 let count = u64::try_from(*count).unwrap_or_default();
92 let start = start.date();
93 if cohorts.last().is_none_or(|cohort| cohort.start != start) {
94 cohorts.push(Cohort {
95 start,
96 size: 0,
97 returned: Vec::new(),
98 });
99 }
100 let cohort = cohorts.last_mut().expect("a cohort was just pushed");
101 match usize::try_from(*offset) {
102 Err(_) => cohort.size = count,
103 Ok(offset) => {
104 if cohort.returned.len() <= offset {
105 cohort.returned.resize(offset + 1, 0);
106 }
107 cohort.returned[offset] = count;
108 }
109 }
110 }
111 Ok(cohorts)
112}
113
114impl crate::Prune {
115 pub async fn product_events(&self, before: sofie_core::Instant) -> Result<u64> {
116 let sql = product_sql(self.db().driver().dialect())?;
117 query_values(self.db().driver(), sql.sweep, &[]).await?;
118 let params = [SqlValue::Timestamp(before.timestamp())];
119 let mut deleted = 0;
120 loop {
121 let counted =
122 query_decoded(self.db().driver(), sql.prune, ¶ms, Vec::new(), |row| {
123 row.int()
124 })
125 .await?;
126 let batch = match counted[..] {
127 [count] => u64::try_from(count)
128 .map_err(|error| Error::internal("counting deleted events", error))?,
129 _ => {
130 return Err(Error::internal_message(
131 "pruning answered no count of deleted events",
132 ));
133 }
134 };
135 if batch == 0 {
136 return Ok(deleted);
137 }
138 deleted += batch;
139 }
140 }
141}
142
143pub(crate) fn product_sql(dialect: &dyn Dialect) -> Result<&'static ProductSql> {
144 dialect.product_events().ok_or_else(|| {
145 Error::internal_message(
146 "`product-reports` needs Postgres: this database keeps no product events",
147 )
148 })
149}
150
151enum Kept {
152 Event(ProductRecord),
153 Flush(oneshot::Sender<()>),
154}
155
156pub struct ProductEvents {
157 queue: mpsc::Sender<Kept>,
158}
159
160impl ProductEvents {
161 pub fn keep(db: &Db) -> Result<Self> {
163 let sql = product_sql(db.driver().dialect())?;
164 let (queue, mut received) = mpsc::channel(QUEUED);
165 let raised = queue.clone();
166 let dropped = Arc::new(AtomicU64::new(0));
167 let installed = keep_product_events(move |record| {
168 if raised.try_send(Kept::Event(record)).is_ok() {
169 return;
170 }
171 let lost = dropped.fetch_add(1, Ordering::Relaxed) + 1;
172 if lost.is_power_of_two() {
173 tracing::warn!(
174 product.dropped = lost,
175 "product events dropped: the database keeps up with fewer"
176 );
177 }
178 });
179 if !installed {
180 return Err(Error::internal_message(
181 "product events are kept already in this process",
182 ));
183 }
184 let db = db.clone();
185 tokio::spawn(async move {
186 let mut batch = Vec::with_capacity(BATCH);
187 while let Some(first) = received.recv().await {
188 let mut flushed = Vec::new();
189 let mut next = Some(first);
190 while let Some(kept) = next {
191 match kept {
192 Kept::Event(record) => batch.push(record),
193 Kept::Flush(done) => flushed.push(done),
194 }
195 next = (batch.len() < BATCH)
196 .then(|| received.try_recv().ok())
197 .flatten();
198 }
199 write(&db, sql, &mut batch).await;
200 for done in flushed {
201 let _ = done.send(());
202 }
203 }
204 });
205 Ok(Self { queue })
206 }
207
208 pub async fn flush(&self) {
209 let (done, written) = oneshot::channel();
210 if self.queue.send(Kept::Flush(done)).await.is_ok() {
211 let _ = written.await;
212 }
213 }
214}
215
216async fn write(db: &Db, sql: &ProductSql, batch: &mut Vec<ProductRecord>) {
217 if batch.is_empty() {
218 return;
219 }
220 let mut at = Vec::with_capacity(batch.len());
221 let mut names = Vec::with_capacity(batch.len());
222 let mut principals = Vec::with_capacity(batch.len());
223 for record in batch.drain(..) {
224 at.push(SqlValue::Timestamp(record.at));
225 names.push(SqlValue::Text(record.name));
226 principals.push(record.principal.map_or(SqlValue::Null, SqlValue::Uuid));
227 }
228 let count = at.len();
229 let params = [
230 SqlValue::Array(at),
231 SqlValue::Array(names),
232 SqlValue::Array(principals),
233 ];
234 if let Err(error) = query_values(db.driver(), sql.insert, ¶ms).await {
235 tracing::warn!(
236 error = %error,
237 product.lost = count,
238 "writing product events failed"
239 );
240 }
241}