Skip to main content

eventuary_sqlite/
partitioning.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::num::NonZeroU32;
4use std::sync::Arc;
5
6use chrono::{DateTime, Utc};
7use rusqlite::types::Value;
8
9use eventuary_core::partition::{PartitionHasher, PartitionKeyResolver, PartitionStrategy};
10use eventuary_core::{Error, Result, SerializedEvent, SerializedPayload};
11
12use crate::database::SqliteConn;
13use crate::event_log::{SqliteEventLogSchema, SqliteEventLogSchemaConfig};
14use crate::relation::SqliteRelationName;
15
16pub struct SqlitePartitionBackfillConfig {
17    pub events_relation: SqliteRelationName,
18    pub partition_count: NonZeroU32,
19    pub key_resolver: Arc<dyn PartitionKeyResolver>,
20    pub hasher: Arc<dyn PartitionHasher>,
21    pub batch_size: usize,
22}
23
24impl SqlitePartitionBackfillConfig {
25    pub fn new(
26        events_relation: SqliteRelationName,
27        partition_count: NonZeroU32,
28        key_resolver: impl PartitionKeyResolver + 'static,
29        hasher: impl PartitionHasher + 'static,
30        batch_size: usize,
31    ) -> Result<Self> {
32        if batch_size == 0 {
33            return Err(Error::Config(
34                "partition backfill batch size must be greater than zero".to_owned(),
35            ));
36        }
37
38        Ok(Self {
39            events_relation,
40            partition_count,
41            key_resolver: Arc::new(key_resolver),
42            hasher: Arc::new(hasher),
43            batch_size,
44        })
45    }
46}
47
48impl fmt::Debug for SqlitePartitionBackfillConfig {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        f.debug_struct("SqlitePartitionBackfillConfig")
51            .field("events_relation", &self.events_relation)
52            .field("partition_count", &self.partition_count)
53            .field("batch_size", &self.batch_size)
54            .finish()
55    }
56}
57
58#[derive(Debug, Clone, Default)]
59pub struct BackfillReport {
60    pub rows_updated: u64,
61    pub batches: u32,
62}
63
64pub struct SqlitePartitionBackfill {
65    conn: SqliteConn,
66    config: SqlitePartitionBackfillConfig,
67}
68
69impl SqlitePartitionBackfill {
70    pub fn new(conn: SqliteConn, config: SqlitePartitionBackfillConfig) -> Self {
71        Self { conn, config }
72    }
73
74    pub fn connect(conn: SqliteConn, config: SqlitePartitionBackfillConfig) -> Result<Self> {
75        Self::prepare_schema(&conn, &config)?;
76        Ok(Self::new(conn, config))
77    }
78
79    pub fn prepare_schema(conn: &SqliteConn, config: &SqlitePartitionBackfillConfig) -> Result<()> {
80        let guard = conn.lock().map_err(|e| Error::Store(e.to_string()))?;
81        SqliteEventLogSchema::prepare(
82            &guard,
83            &SqliteEventLogSchemaConfig {
84                events_relation: config.events_relation.clone(),
85            },
86        )
87    }
88
89    pub fn schema_sql(config: &SqlitePartitionBackfillConfig) -> String {
90        SqliteEventLogSchema::schema_sql(&SqliteEventLogSchemaConfig {
91            events_relation: config.events_relation.clone(),
92        })
93    }
94
95    pub async fn run(&self) -> Result<BackfillReport> {
96        let events = self.config.events_relation.render();
97        let batch_size = self.config.batch_size;
98        let partition_count = self.config.partition_count;
99        let key_resolver = Arc::clone(&self.config.key_resolver);
100        let hasher = Arc::clone(&self.config.hasher);
101        let conn = Arc::clone(&self.conn);
102
103        let fetch_sql = format!(
104            "SELECT sequence, id, organization, namespace, topic, event_key, payload, \
105             content_type, metadata, timestamp, version, parent_id, correlation_id, causation_id \
106             FROM {events} \
107             WHERE partition_id IS NULL \
108             ORDER BY sequence \
109             LIMIT ?1"
110        );
111
112        let update_sql = format!(
113            "UPDATE {events} \
114             SET partition_key = ?1, \
115                 partition_hash = ?2, \
116                 partition_id = ?3, \
117                 partition_count = ?4, \
118                 partition_strategy = ?5 \
119             WHERE sequence = ?6 AND partition_id IS NULL"
120        );
121
122        tokio::task::spawn_blocking(move || {
123            let mut report = BackfillReport::default();
124            let mut guard = conn.lock().map_err(|e| Error::Store(e.to_string()))?;
125
126            loop {
127                let rows = {
128                    let mut stmt = guard
129                        .prepare(&fetch_sql)
130                        .map_err(|e| Error::Store(e.to_string()))?;
131                    let mapped = stmt
132                        .query_map([Value::Integer(batch_size as i64)], decode_row)
133                        .map_err(|e| Error::Store(e.to_string()))?;
134                    let mut out: Vec<(i64, SerializedEvent)> = Vec::new();
135                    for row in mapped {
136                        out.push(row.map_err(|e| Error::Store(e.to_string()))?);
137                    }
138                    out
139                };
140
141                if rows.is_empty() {
142                    break;
143                }
144
145                let tx = guard
146                    .transaction()
147                    .map_err(|e| Error::Store(e.to_string()))?;
148
149                for (sequence, serialized) in &rows {
150                    let event = serialized.to_event()?;
151                    let partition_key = key_resolver.partition_key(&event)?;
152                    let partition_hash = hasher.hash(&partition_key);
153                    let partition = hasher.partition_for(&partition_key, partition_count);
154                    let partition_strategy = PartitionStrategy::new(hasher.strategy())?;
155
156                    let updated = tx
157                        .execute(
158                            &update_sql,
159                            rusqlite::params![
160                                partition_key.as_str(),
161                                partition_hash.to_sql_i64(),
162                                partition.id() as i64,
163                                partition.count() as i64,
164                                partition_strategy.as_str(),
165                                *sequence,
166                            ],
167                        )
168                        .map_err(|e| Error::Store(e.to_string()))?;
169                    report.rows_updated += updated as u64;
170                }
171
172                tx.commit().map_err(|e| Error::Store(e.to_string()))?;
173                report.batches += 1;
174            }
175
176            Ok(report)
177        })
178        .await
179        .map_err(|e| Error::Store(format!("blocking task panicked: {e}")))?
180    }
181}
182
183fn decode_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<(i64, SerializedEvent)> {
184    let sequence: i64 = row.get(0)?;
185    let id: String = row.get(1)?;
186    let organization: String = row.get(2)?;
187    let namespace: String = row.get(3)?;
188    let topic: String = row.get(4)?;
189    let key: String = row.get(5)?;
190    let payload_str: String = row.get(6)?;
191    let _content_type: String = row.get(7)?;
192    let metadata_str: String = row.get(8)?;
193    let timestamp_str: String = row.get(9)?;
194    let version: i64 = row.get(10)?;
195    let parent_id: Option<String> = row.get(11)?;
196    let correlation_id: Option<String> = row.get(12)?;
197    let causation_id: Option<String> = row.get(13)?;
198
199    let id = uuid::Uuid::parse_str(&id).map_err(|e| {
200        rusqlite::Error::FromSqlConversionFailure(
201            1,
202            rusqlite::types::Type::Text,
203            Box::new(std::io::Error::other(format!("decode id: {e}"))),
204        )
205    })?;
206    let parent_id = parent_id
207        .as_deref()
208        .map(uuid::Uuid::parse_str)
209        .transpose()
210        .map_err(|e| {
211            rusqlite::Error::FromSqlConversionFailure(
212                11,
213                rusqlite::types::Type::Text,
214                Box::new(std::io::Error::other(format!("decode parent_id: {e}"))),
215            )
216        })?;
217    let payload: SerializedPayload = serde_json::from_str(&payload_str).map_err(|e| {
218        rusqlite::Error::FromSqlConversionFailure(
219            6,
220            rusqlite::types::Type::Text,
221            Box::new(std::io::Error::other(format!("decode payload: {e}"))),
222        )
223    })?;
224    let metadata: HashMap<String, String> = serde_json::from_str(&metadata_str).map_err(|e| {
225        rusqlite::Error::FromSqlConversionFailure(
226            8,
227            rusqlite::types::Type::Text,
228            Box::new(std::io::Error::other(format!("decode metadata: {e}"))),
229        )
230    })?;
231    let timestamp = DateTime::parse_from_rfc3339(&timestamp_str)
232        .map(|d| d.with_timezone(&Utc))
233        .map_err(|e| {
234            rusqlite::Error::FromSqlConversionFailure(
235                9,
236                rusqlite::types::Type::Text,
237                Box::new(std::io::Error::other(format!("decode timestamp: {e}"))),
238            )
239        })?;
240
241    Ok((
242        sequence,
243        SerializedEvent {
244            id,
245            organization,
246            namespace,
247            topic,
248            payload,
249            metadata,
250            timestamp,
251            version: version as u64,
252            key,
253            parent_id,
254            correlation_id,
255            causation_id,
256        },
257    ))
258}