systemprompt_analytics/projection/
state.rs1use chrono::{DateTime, Utc};
13use serde::Serialize;
14use sqlx::{PgConnection, PgPool};
15
16use crate::{AnalyticsError, Result};
17
18const PROJECTOR_LOCK: i64 = 0x5350_414e_414c_5954;
19
20#[derive(Debug, Clone, Serialize)]
22pub struct ProjectionStatus {
23 pub initialized: bool,
24 pub generation: i64,
25 pub rebuilt_at: Option<DateTime<Utc>>,
26 pub rebuild_started_at: Option<DateTime<Utc>>,
27 pub rebuild_heartbeat_at: Option<DateTime<Utc>>,
28 pub rebuild_source: Option<String>,
29 pub rebuild_rows: i64,
30 pub pending_count: i64,
31 pub applied_last_minute: i64,
32 pub oldest_pending_at: Option<DateTime<Utc>>,
33 pub last_processed_at: Option<DateTime<Utc>>,
34}
35
36#[derive(Debug, Clone, Copy)]
38pub struct RebuildState {
39 pub generation: i64,
40 pub initialized: bool,
41 pub rebuild_started_at: Option<DateTime<Utc>>,
42 pub rebuild_heartbeat_at: Option<DateTime<Utc>>,
43}
44
45pub async fn lock_projector(connection: &mut PgConnection) -> Result<()> {
46 sqlx::query!("SELECT pg_advisory_xact_lock($1)", PROJECTOR_LOCK)
47 .fetch_one(connection)
48 .await?;
49 Ok(())
50}
51
52pub async fn lock_user_deletion(connection: &mut PgConnection) -> Result<()> {
53 sqlx::query_scalar!(r#"SELECT public.lock_user_deletion_for_retention() AS "locked!""#)
54 .fetch_one(connection)
55 .await?;
56 Ok(())
57}
58
59pub async fn is_initialized(connection: &mut PgConnection) -> Result<bool> {
60 Ok(sqlx::query_scalar!(
61 r#"SELECT initialized AS "initialized!" FROM analytics_projection_state WHERE singleton"#
62 )
63 .fetch_one(connection)
64 .await?)
65}
66
67pub async fn rebuild_state(connection: &mut PgConnection) -> Result<RebuildState> {
68 Ok(sqlx::query_as!(
69 RebuildState,
70 "SELECT generation, initialized, rebuild_started_at, rebuild_heartbeat_at
71 FROM analytics_projection_state WHERE singleton"
72 )
73 .fetch_one(connection)
74 .await?)
75}
76
77pub async fn heartbeat_rebuild(
78 connection: &mut PgConnection,
79 generation: i64,
80 source: &str,
81 rows: i64,
82) -> Result<()> {
83 let result = sqlx::query!(
84 "UPDATE analytics_projection_state
85 SET rebuild_heartbeat_at = NOW(), rebuild_source = $2, rebuild_rows = rebuild_rows + $3
86 WHERE singleton AND generation = $1 AND NOT initialized AND rebuild_started_at IS NOT NULL",
87 generation,
88 source,
89 rows
90 )
91 .execute(connection)
92 .await?;
93 if result.rows_affected() != 1 {
94 return Err(AnalyticsError::rebuild_superseded());
95 }
96 Ok(())
97}
98
99pub async fn next_cutoff_revision(connection: &mut PgConnection) -> Result<i64> {
100 Ok(
101 sqlx::query_scalar!(r#"SELECT nextval('event_outbox_reporting_revision') AS "cutoff!""#)
102 .fetch_one(connection)
103 .await?,
104 )
105}
106
107pub async fn status(pool: &PgPool, consumer: &str) -> Result<ProjectionStatus> {
108 Ok(sqlx::query_as!(
109 ProjectionStatus,
110 r#"SELECT initialized, generation, rebuilt_at,
111 rebuild_started_at, rebuild_heartbeat_at, rebuild_source, rebuild_rows,
112 (SELECT COUNT(*) FROM event_outbox WHERE consumer = $1 AND processed_at IS NULL) AS "pending_count!",
113 (SELECT COUNT(*) FROM event_outbox WHERE consumer = $1 AND processed_at >= NOW() - INTERVAL '1 minute') AS "applied_last_minute!",
114 (SELECT MIN(created_at) FROM event_outbox WHERE consumer = $1 AND processed_at IS NULL) AS oldest_pending_at,
115 (SELECT MAX(processed_at) FROM event_outbox WHERE consumer = $1) AS last_processed_at
116 FROM analytics_projection_state WHERE singleton"#,
117 consumer
118 )
119 .fetch_one(pool)
120 .await?)
121}