1use std::{collections::HashMap, time::Duration};
35
36use eventcore_types::{
37 CheckpointStore, CommandStateReplayCheckpoint, CommandStateSnapshot, CommandStateSnapshotId,
38 Event, EventFilter, EventPage, EventReader, EventStore, EventStoreError, EventStream,
39 EventStreamSlice, Operation, ProjectorCoordinator, StreamId, StreamPosition, StreamVersion,
40 StreamWrites,
41};
42use futures::StreamExt;
43use nutype::nutype;
44use serde_json::value::RawValue;
45use serde_json::{Value, json};
46use sqlx::types::Json;
47use sqlx::{Pool, Postgres, QueryBuilder, Row, postgres::PgPoolOptions, query};
48use thiserror::Error;
49use tracing::{error, info, instrument, warn};
50use uuid::Uuid;
51
52pub mod projections;
53pub use projections::{
54 AfterCommit, BoxedProjectionError, NoopAfterCommit, PostgresProjectionConfig,
55 PostgresProjectionMode, PostgresProjectionReset, PostgresProjectionSource,
56 PostgresProjectionSourceError, PostgresProjectionStore, PostgresProjector,
57 ProjectionConfigurationError, ProjectionFailureContext, ProjectionFailureDecision,
58 ProjectionPollSleeper, ProjectionProgress, ProjectionResetAndReplayError, ProjectionResetError,
59 ProjectionRetryPolicy, ProjectionRetrySleeper, ProjectionRunOutcome,
60 TokioProjectionPollSleeper, TokioProjectionRetrySleeper, TransactionalProjectionError,
61 reset_and_replay_transactional_projection, reset_transactional_projection,
62 run_transactional_projection,
63};
64
65#[derive(Debug, Error)]
67pub enum PostgresEventStoreError {
68 #[error("failed to create postgres connection pool")]
70 ConnectionFailed(#[source] sqlx::Error),
71}
72
73#[nutype(derive(Debug, Clone, Copy, PartialEq, Eq, Display, AsRef, Into))]
92pub struct MaxConnections(std::num::NonZeroU32);
93
94#[derive(Debug, Clone)]
96pub struct PostgresConfig {
97 pub max_connections: MaxConnections,
99 pub acquire_timeout: Duration,
101 pub idle_timeout: Duration,
103}
104
105impl Default for PostgresConfig {
106 fn default() -> Self {
107 const DEFAULT_MAX_CONNECTIONS: std::num::NonZeroU32 = match std::num::NonZeroU32::new(10) {
108 Some(v) => v,
109 None => unreachable!(),
110 };
111
112 Self {
113 max_connections: MaxConnections::new(DEFAULT_MAX_CONNECTIONS),
114 acquire_timeout: Duration::from_secs(30),
115 idle_timeout: Duration::from_secs(600), }
117 }
118}
119
120#[derive(Debug, Clone)]
128pub struct PostgresEventStore {
129 pool: Pool<Postgres>,
130}
131
132impl PostgresEventStore {
133 pub async fn new<S: Into<String>>(
135 connection_string: S,
136 ) -> Result<Self, PostgresEventStoreError> {
137 Self::with_config(connection_string, PostgresConfig::default()).await
138 }
139
140 pub async fn with_config<S: Into<String>>(
142 connection_string: S,
143 config: PostgresConfig,
144 ) -> Result<Self, PostgresEventStoreError> {
145 let connection_string = connection_string.into();
146 let max_connections: std::num::NonZeroU32 = config.max_connections.into();
147 let pool = PgPoolOptions::new()
148 .max_connections(max_connections.get())
149 .acquire_timeout(config.acquire_timeout)
150 .idle_timeout(config.idle_timeout)
151 .connect(&connection_string)
152 .await
153 .map_err(PostgresEventStoreError::ConnectionFailed)?;
154 Ok(Self { pool })
155 }
156
157 pub fn from_pool(pool: Pool<Postgres>) -> Self {
162 Self { pool }
163 }
164
165 #[cfg_attr(test, mutants::skip)] pub async fn ping(&self) {
172 let _ = query("SELECT 1")
173 .execute(&self.pool)
174 .await
175 .expect("postgres ping failed");
176 }
177
178 #[cfg_attr(test, mutants::skip)] pub async fn migrate(&self) {
188 sqlx::migrate!("./migrations")
189 .run(&self.pool)
190 .await
191 .expect("postgres migration failed");
192 }
193}
194
195impl EventStore for PostgresEventStore {
196 #[instrument(name = "postgres.read_stream", skip(self))]
197 async fn read_stream<E: Event>(
198 &self,
199 stream_id: StreamId,
200 ) -> Result<EventStream<E>, EventStoreError> {
201 info!(
202 stream = %stream_id,
203 "[postgres.read_stream] reading events from postgres"
204 );
205
206 let pool = self.pool.clone();
212
213 let stream = async_stream::stream! {
214 let mut rows = query(
215 "SELECT event_data FROM eventcore_events WHERE stream_id = $1 ORDER BY stream_version ASC",
216 )
217 .bind(stream_id.as_ref())
218 .fetch(&pool);
219
220 while let Some(row) = rows.next().await {
221 let row = match row {
222 Ok(row) => row,
223 Err(error) => {
224 yield Err(map_sqlx_error(error, Operation::ReadStream));
225 break;
226 }
227 };
228
229 let payload: Value = match row.try_get("event_data") {
230 Ok(payload) => payload,
231 Err(error) => {
232 yield Err(map_sqlx_error(error, Operation::ReadStream));
233 break;
234 }
235 };
236
237 match serde_json::from_value::<E>(payload) {
241 Ok(event) => yield Ok(event),
242 Err(error) => {
243 yield Err(EventStoreError::DeserializationFailed {
244 stream_id: stream_id.clone(),
245 detail: error.to_string(),
246 });
247 break;
248 }
249 }
250 }
251 };
252
253 Ok(EventStream::new(stream))
254 }
255
256 #[instrument(name = "postgres.read_stream_after", skip(self))]
257 async fn read_stream_after<E: Event>(
258 &self,
259 stream_id: StreamId,
260 exclusive_version: StreamVersion,
261 ) -> Result<EventStream<E>, EventStoreError> {
262 let pool = self.pool.clone();
263 let version: usize = exclusive_version.into();
264
265 let stream = async_stream::stream! {
266 let mut rows = query(
267 "SELECT event_data FROM eventcore_events \
268 WHERE stream_id = $1 AND stream_version > $2 \
269 ORDER BY stream_version ASC",
270 )
271 .bind(stream_id.as_ref())
272 .bind(version as i64)
273 .fetch(&pool);
274
275 while let Some(row) = rows.next().await {
276 let row = match row {
277 Ok(row) => row,
278 Err(error) => {
279 yield Err(map_sqlx_error(error, Operation::ReadStream));
280 break;
281 }
282 };
283
284 let payload: Value = match row.try_get("event_data") {
285 Ok(payload) => payload,
286 Err(error) => {
287 yield Err(map_sqlx_error(error, Operation::ReadStream));
288 break;
289 }
290 };
291
292 match serde_json::from_value::<E>(payload) {
293 Ok(event) => yield Ok(event),
294 Err(error) => {
295 yield Err(EventStoreError::DeserializationFailed {
296 stream_id: stream_id.clone(),
297 detail: error.to_string(),
298 });
299 break;
300 }
301 }
302 }
303 };
304
305 Ok(EventStream::new(stream))
306 }
307
308 #[instrument(name = "postgres.append_events", skip(self, writes))]
309 async fn append_events(
310 &self,
311 writes: StreamWrites,
312 ) -> Result<EventStreamSlice, EventStoreError> {
313 let expected_versions = writes.expected_versions().clone();
314 let entries = writes.into_entries();
315
316 if entries.is_empty() {
317 return Ok(EventStreamSlice);
318 }
319
320 info!(
321 stream_count = expected_versions.len(),
322 event_count = entries.len(),
323 "[postgres.append_events] appending events to postgres"
324 );
325
326 let expected_versions_json: Value = expected_versions
328 .iter()
329 .map(|(stream_id, version)| {
330 (stream_id.as_ref().to_string(), json!(version.into_inner()))
331 })
332 .collect();
333
334 let mut tx = self
335 .pool
336 .begin()
337 .await
338 .map_err(|error| map_sqlx_error(error, Operation::BeginTransaction))?;
339
340 validate_expected_versions(&mut tx, &expected_versions).await?;
341
342 let _ = query("SELECT set_config('eventcore.expected_versions', $1, true)")
344 .bind(expected_versions_json.to_string())
345 .execute(&mut *tx)
346 .await
347 .map_err(|error| map_sqlx_error(error, Operation::SetExpectedVersions))?;
348
349 let rows: Vec<(StreamId, &'static str, Box<RawValue>)> = entries
359 .into_iter()
360 .map(|entry| (entry.stream_id, entry.event_type, entry.event_data))
361 .collect();
362
363 const MAX_EVENTS_PER_INSERT: usize = 1000;
364 for chunk in rows.chunks(MAX_EVENTS_PER_INSERT) {
365 let mut builder = QueryBuilder::<Postgres>::new(
366 "INSERT INTO eventcore_events (event_id, stream_id, event_type, event_data, metadata) ",
367 );
368 let _ = builder.push_values(chunk, |mut row, (stream_id, event_type, event_data)| {
369 let _ = row
370 .push_bind(Uuid::now_v7())
371 .push_bind(stream_id.as_ref())
372 .push_bind(*event_type)
373 .push_bind(Json(event_data))
374 .push_bind(Json(json!({})));
375 });
376 let _ = builder
377 .build()
378 .execute(&mut *tx)
379 .await
380 .map_err(|error| map_sqlx_error(error, Operation::AppendEvents))?;
381 }
382
383 tx.commit()
384 .await
385 .map_err(|error| map_sqlx_error(error, Operation::CommitTransaction))?;
386
387 Ok(EventStreamSlice)
388 }
389
390 async fn load_command_state_snapshot(
391 &self,
392 snapshot_id: CommandStateSnapshotId,
393 ) -> Result<Option<CommandStateSnapshot>, EventStoreError> {
394 let row = query(
395 "SELECT state, stream_versions, replay_checkpoints \
396 FROM eventcore_command_state_snapshots \
397 WHERE snapshot_id = $1",
398 )
399 .bind(snapshot_id.as_ref())
400 .fetch_optional(&self.pool)
401 .await
402 .map_err(|error| map_sqlx_error(error, Operation::ReadStream))?;
403
404 row.map(|row| {
405 let state: Json<Value> = row
406 .try_get("state")
407 .map_err(|error| map_sqlx_error(error, Operation::ReadStream))?;
408 let stream_versions: Json<HashMap<StreamId, StreamVersion>> = row
409 .try_get("stream_versions")
410 .map_err(|error| map_sqlx_error(error, Operation::ReadStream))?;
411 let replay_checkpoints: Json<Vec<CommandStateReplayCheckpoint>> = row
412 .try_get("replay_checkpoints")
413 .map_err(|error| map_sqlx_error(error, Operation::ReadStream))?;
414
415 Ok(CommandStateSnapshot::new(state.0, stream_versions.0)
416 .with_replay_checkpoints(replay_checkpoints.0))
417 })
418 .transpose()
419 }
420
421 async fn save_command_state_snapshot(
422 &self,
423 snapshot_id: CommandStateSnapshotId,
424 snapshot: CommandStateSnapshot,
425 ) -> Result<(), EventStoreError> {
426 let _ = query(
430 "INSERT INTO eventcore_command_state_snapshots \
431 (snapshot_id, state, stream_versions, replay_checkpoints, updated_at) \
432 VALUES ($1, $2, $3, $4, NOW()) \
433 ON CONFLICT (snapshot_id) DO UPDATE \
434 SET state = EXCLUDED.state, \
435 stream_versions = EXCLUDED.stream_versions, \
436 replay_checkpoints = EXCLUDED.replay_checkpoints, \
437 updated_at = NOW() \
438 WHERE NOT EXISTS ( \
439 SELECT 1 \
440 FROM jsonb_each_text(eventcore_command_state_snapshots.stream_versions) \
441 AS current(stream_id, stream_version) \
442 WHERE NOT (EXCLUDED.stream_versions ? current.stream_id) \
443 OR (EXCLUDED.stream_versions ->> current.stream_id)::BIGINT \
444 < current.stream_version::BIGINT \
445 )",
446 )
447 .bind(snapshot_id.as_ref())
448 .bind(Json(snapshot.state))
449 .bind(Json(snapshot.stream_versions))
450 .bind(Json(snapshot.replay_checkpoints))
451 .execute(&self.pool)
452 .await
453 .map_err(|error| map_sqlx_error(error, Operation::AppendEvents))?;
454
455 Ok(())
456 }
457}
458
459impl CheckpointStore for PostgresEventStore {
460 type Error = PostgresCheckpointError;
461
462 async fn load(&self, name: &str) -> Result<Option<StreamPosition>, Self::Error> {
463 let row = query("SELECT last_position FROM eventcore_subscription_versions WHERE subscription_name = $1")
464 .bind(name)
465 .fetch_optional(&self.pool)
466 .await
467 .map_err(PostgresCheckpointError::DatabaseError)?;
468
469 match row {
470 Some(row) => {
471 let position: Uuid = row.get("last_position");
472 Ok(Some(StreamPosition::new(position)))
473 }
474 None => Ok(None),
475 }
476 }
477
478 async fn save(&self, name: &str, position: StreamPosition) -> Result<(), Self::Error> {
479 let position_uuid: Uuid = position.into_inner();
480 let _ = query(
481 "INSERT INTO eventcore_subscription_versions (subscription_name, last_position, updated_at)
482 VALUES ($1, $2, NOW())
483 ON CONFLICT (subscription_name) DO UPDATE SET last_position = $2, updated_at = NOW()",
484 )
485 .bind(name)
486 .bind(position_uuid)
487 .execute(&self.pool)
488 .await
489 .map_err(PostgresCheckpointError::DatabaseError)?;
490
491 Ok(())
492 }
493}
494
495async fn validate_expected_versions(
505 tx: &mut sqlx::Transaction<'_, Postgres>,
506 expected_versions: &HashMap<StreamId, StreamVersion>,
507) -> Result<(), EventStoreError> {
508 let mut expected_streams: Vec<_> = expected_versions.iter().collect();
509 expected_streams.sort_unstable_by(|(left, _), (right, _)| left.as_ref().cmp(right.as_ref()));
510
511 for (stream_id, _) in &expected_streams {
512 let _ = query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))")
513 .bind(stream_id.as_ref())
514 .execute(&mut **tx)
515 .await
516 .map_err(|error| map_sqlx_error(error, Operation::AppendEvents))?;
517 }
518
519 for (stream_id, expected_version) in expected_streams {
520 let current_version: i64 = sqlx::query_scalar(
521 "SELECT COALESCE(MAX(stream_version), 0) FROM eventcore_events WHERE stream_id = $1",
522 )
523 .bind(stream_id.as_ref())
524 .fetch_one(&mut **tx)
525 .await
526 .map_err(|error| map_sqlx_error(error, Operation::AppendEvents))?;
527 let current_version =
528 usize::try_from(current_version).map_err(|_| EventStoreError::StoreFailure {
529 operation: Operation::AppendEvents,
530 })?;
531 let actual = StreamVersion::new(current_version);
532
533 if actual != *expected_version {
534 warn!(
535 stream = %stream_id,
536 expected = expected_version.into_inner(),
537 actual = actual.into_inner(),
538 "[postgres.version_conflict] optimistic concurrency check failed"
539 );
540 return Err(EventStoreError::VersionConflict {
541 stream_id: stream_id.clone(),
542 expected: *expected_version,
543 actual,
544 });
545 }
546 }
547
548 Ok(())
549}
550
551impl EventReader for PostgresEventStore {
552 type Error = EventStoreError;
553
554 async fn read_events<E: Event>(
555 &self,
556 filter: EventFilter,
557 page: EventPage,
558 ) -> Result<Vec<(E, StreamPosition)>, Self::Error> {
559 let after_event_id: Option<Uuid> = page.after_position().map(|p| p.into_inner());
562 let limit: i64 = page.limit().into_inner() as i64;
563
564 let type_filter = filter.event_type().unwrap_or_else(|| E::event_type_name());
568
569 let pattern_regex = filter
574 .stream_pattern()
575 .map(|p| glob_to_anchored_regex(p.as_ref()));
576
577 let mut builder = QueryBuilder::<Postgres>::new(
581 "SELECT event_id, event_data, stream_id FROM eventcore_events WHERE event_type = ",
582 );
583 let _ = builder.push_bind(type_filter);
584
585 if let Some(after_id) = after_event_id {
586 let _ = builder.push(" AND event_id > ").push_bind(after_id);
587 }
588
589 if let Some(prefix) = filter.stream_prefix() {
590 let _ = builder
591 .push(" AND stream_id LIKE ")
592 .push_bind(prefix.as_ref().to_string())
593 .push(" || '%'");
594 } else if let Some(regex) = pattern_regex {
595 let _ = builder.push(" AND stream_id ~ ").push_bind(regex);
596 }
597
598 let _ = builder.push(" ORDER BY event_id LIMIT ").push_bind(limit);
599
600 let rows = builder
601 .build()
602 .fetch_all(&self.pool)
603 .await
604 .map_err(|error| map_sqlx_error(error, Operation::ReadStream))?;
605
606 let events: Vec<(E, StreamPosition)> = rows
607 .into_iter()
608 .filter_map(|row| {
609 let event_data: Json<Value> = row.get("event_data");
610 let event_id: Uuid = row.get("event_id");
611 serde_json::from_value::<E>(event_data.0)
612 .ok()
613 .map(|e| (e, StreamPosition::new(event_id)))
614 })
615 .collect();
616
617 Ok(events)
618 }
619}
620
621fn glob_to_anchored_regex(glob: &str) -> String {
637 let mut regex = String::with_capacity(glob.len() + 2);
638 regex.push('^');
639
640 let mut chars = glob.chars().peekable();
641 while let Some(c) = chars.next() {
642 match c {
643 '*' => regex.push_str(".*"),
644 '?' => regex.push('.'),
645 '[' => {
646 let mut class = String::new();
648 let mut closed = false;
649 if matches!(chars.peek(), Some('!')) {
650 let _ = chars.next();
651 class.push('^');
652 }
653 for inner in chars.by_ref() {
654 if inner == ']' {
655 closed = true;
656 break;
657 }
658 class.push(inner);
659 }
660 if closed {
661 regex.push('[');
662 regex.push_str(&class);
663 regex.push(']');
664 } else {
665 regex.push_str(®ex_escape("["));
668 regex.push_str(®ex_escape(&class));
669 }
670 }
671 other => regex.push_str(®ex_escape(&other.to_string())),
672 }
673 }
674
675 regex.push('$');
676 regex
677}
678
679fn regex_escape(literal: &str) -> String {
682 const METACHARACTERS: &[char] = &[
683 '.', '^', '$', '*', '+', '?', '(', ')', '[', ']', '{', '}', '|', '\\',
684 ];
685 let mut escaped = String::with_capacity(literal.len());
686 for c in literal.chars() {
687 if METACHARACTERS.contains(&c) {
688 escaped.push('\\');
689 }
690 escaped.push(c);
691 }
692 escaped
693}
694
695fn map_sqlx_error(error: sqlx::Error, operation: Operation) -> EventStoreError {
696 if let sqlx::Error::Database(db_error) = &error {
697 let code = db_error.code();
698 let code_str = code.as_deref();
699 if code_str == Some("P0001") || code_str == Some("23505") {
702 warn!(
703 error = %db_error,
704 "[postgres.version_conflict] optimistic concurrency check failed"
705 );
706 return parse_version_conflict_from_db_error(db_error.message());
707 }
708 }
709
710 error!(
711 error = %error,
712 operation = %operation,
713 "[postgres.database_error] database operation failed"
714 );
715 EventStoreError::StoreFailure { operation }
716}
717
718fn parse_version_conflict_from_db_error(message: &str) -> EventStoreError {
726 if let Some(parsed) = try_parse_conflict_message(message) {
728 return parsed;
729 }
730
731 let fallback_stream_id =
734 StreamId::try_new("unknown-conflict-stream").expect("static stream id is valid");
735 EventStoreError::VersionConflict {
736 stream_id: fallback_stream_id,
737 expected: StreamVersion::new(0),
738 actual: StreamVersion::new(0),
739 }
740}
741
742fn try_parse_conflict_message(message: &str) -> Option<EventStoreError> {
743 let rest = message.strip_prefix("version_conflict: stream \"")?;
744 let stream_end = rest.find('"')?;
745 let stream_id_str = &rest[..stream_end];
746 let after_stream = &rest[stream_end..];
747
748 let expected_str = after_stream
749 .strip_prefix("\" expected version ")?
750 .split(',')
751 .next()?;
752 let actual_str = after_stream.rsplit("actual ").next()?;
753
754 let expected = expected_str.trim().parse::<usize>().ok()?;
755 let actual = actual_str.trim().parse::<usize>().ok()?;
756 let stream_id = StreamId::try_new(stream_id_str).ok()?;
757
758 Some(EventStoreError::VersionConflict {
759 stream_id,
760 expected: StreamVersion::new(expected),
761 actual: StreamVersion::new(actual),
762 })
763}
764
765#[derive(Debug, Error)]
767pub enum PostgresCheckpointError {
768 #[error("failed to create postgres connection pool")]
770 ConnectionFailed(#[source] sqlx::Error),
771
772 #[error("database operation failed: {0}")]
774 DatabaseError(#[source] sqlx::Error),
775}
776
777#[derive(Debug, Clone)]
791pub struct PostgresCheckpointStore {
792 pool: Pool<Postgres>,
793}
794
795impl PostgresCheckpointStore {
796 pub async fn new<S: Into<String>>(
798 connection_string: S,
799 ) -> Result<Self, PostgresCheckpointError> {
800 Self::with_config(connection_string, PostgresConfig::default()).await
801 }
802
803 pub async fn with_config<S: Into<String>>(
805 connection_string: S,
806 config: PostgresConfig,
807 ) -> Result<Self, PostgresCheckpointError> {
808 let connection_string = connection_string.into();
809 let max_connections: std::num::NonZeroU32 = config.max_connections.into();
810 let pool = PgPoolOptions::new()
811 .max_connections(max_connections.get())
812 .acquire_timeout(config.acquire_timeout)
813 .idle_timeout(config.idle_timeout)
814 .connect(&connection_string)
815 .await
816 .map_err(PostgresCheckpointError::ConnectionFailed)?;
817
818 sqlx::migrate!("./migrations")
820 .run(&pool)
821 .await
822 .map_err(|e| {
823 PostgresCheckpointError::DatabaseError(sqlx::Error::Migrate(Box::new(e)))
824 })?;
825
826 Ok(Self { pool })
827 }
828
829 pub fn from_pool(pool: Pool<Postgres>) -> Self {
834 Self { pool }
835 }
836}
837
838impl CheckpointStore for PostgresCheckpointStore {
839 type Error = PostgresCheckpointError;
840
841 async fn load(&self, name: &str) -> Result<Option<StreamPosition>, Self::Error> {
842 let row = query("SELECT last_position FROM eventcore_subscription_versions WHERE subscription_name = $1")
843 .bind(name)
844 .fetch_optional(&self.pool)
845 .await
846 .map_err(PostgresCheckpointError::DatabaseError)?;
847
848 match row {
849 Some(row) => {
850 let position: Uuid = row.get("last_position");
851 Ok(Some(StreamPosition::new(position)))
852 }
853 None => Ok(None),
854 }
855 }
856
857 async fn save(&self, name: &str, position: StreamPosition) -> Result<(), Self::Error> {
858 let position_uuid: Uuid = position.into_inner();
859 let _ = query(
860 "INSERT INTO eventcore_subscription_versions (subscription_name, last_position, updated_at)
861 VALUES ($1, $2, NOW())
862 ON CONFLICT (subscription_name) DO UPDATE SET last_position = $2, updated_at = NOW()",
863 )
864 .bind(name)
865 .bind(position_uuid)
866 .execute(&self.pool)
867 .await
868 .map_err(PostgresCheckpointError::DatabaseError)?;
869
870 Ok(())
871 }
872}
873
874#[derive(Debug, Error)]
880pub enum CoordinationError {
881 #[error(
883 "leadership not acquired for subscription '{subscription_name}': another instance holds the lock"
884 )]
885 LeadershipNotAcquired { subscription_name: String },
886
887 #[error("database operation failed: {0}")]
889 DatabaseError(#[source] sqlx::Error),
890}
891
892pub struct CoordinationGuard {
922 lock_key: i64,
923 connection: Option<sqlx::pool::PoolConnection<Postgres>>,
926}
927
928impl std::fmt::Debug for CoordinationGuard {
929 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
930 f.debug_struct("CoordinationGuard")
931 .field("lock_key", &self.lock_key)
932 .finish_non_exhaustive()
933 }
934}
935
936impl Drop for CoordinationGuard {
937 fn drop(&mut self) {
938 if let Some(mut connection) = self.connection.take() {
940 let lock_key = self.lock_key;
941
942 let handle = tokio::runtime::Handle::current();
945 let is_multi_thread =
946 handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread;
947
948 if is_multi_thread {
949 tokio::task::block_in_place(|| {
951 handle.block_on(async {
952 if let Err(e) = query("SELECT pg_advisory_unlock($1)")
954 .bind(lock_key)
955 .execute(&mut *connection)
956 .await
957 {
958 warn!(
959 lock_key = lock_key,
960 error = %e,
961 "failed to release advisory lock on drop"
962 );
963 }
964 });
966 });
967 } else {
968 drop(tokio::spawn(async move {
973 if let Err(e) = query("SELECT pg_advisory_unlock($1)")
974 .bind(lock_key)
975 .execute(&mut *connection)
976 .await
977 {
978 warn!(
979 lock_key = lock_key,
980 error = %e,
981 "failed to release advisory lock on drop (async)"
982 );
983 }
984 }));
985 }
986 }
987 }
988}
989
990#[derive(Debug, Clone)]
996pub struct PostgresProjectorCoordinator {
997 pool: Pool<Postgres>,
998}
999
1000impl PostgresProjectorCoordinator {
1001 pub async fn new<S: Into<String>>(connection_string: S) -> Result<Self, CoordinationError> {
1003 Self::with_config(connection_string, PostgresConfig::default()).await
1004 }
1005
1006 pub async fn with_config<S: Into<String>>(
1008 connection_string: S,
1009 config: PostgresConfig,
1010 ) -> Result<Self, CoordinationError> {
1011 let connection_string = connection_string.into();
1012 let max_connections: std::num::NonZeroU32 = config.max_connections.into();
1013 let pool = PgPoolOptions::new()
1014 .max_connections(max_connections.get())
1015 .acquire_timeout(config.acquire_timeout)
1016 .idle_timeout(config.idle_timeout)
1017 .connect(&connection_string)
1018 .await
1019 .map_err(CoordinationError::DatabaseError)?;
1020
1021 Ok(Self { pool })
1022 }
1023
1024 pub fn from_pool(pool: Pool<Postgres>) -> Self {
1026 Self { pool }
1027 }
1028}
1029
1030fn advisory_lock_key(subscription_name: &str) -> i64 {
1035 const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
1036 const FNV_PRIME: u64 = 0x00000100000001B3;
1037
1038 let mut hash = FNV_OFFSET_BASIS;
1039 for byte in subscription_name.as_bytes() {
1040 hash ^= *byte as u64;
1041 hash = hash.wrapping_mul(FNV_PRIME);
1042 }
1043 hash as i64
1044}
1045
1046async fn try_acquire_advisory_lock(
1049 pool: &Pool<Postgres>,
1050 subscription_name: &str,
1051) -> Result<CoordinationGuard, CoordinationError> {
1052 let lock_key = advisory_lock_key(subscription_name);
1053
1054 let mut connection = pool
1058 .acquire()
1059 .await
1060 .map_err(CoordinationError::DatabaseError)?;
1061
1062 let row = query("SELECT pg_try_advisory_lock($1)")
1064 .bind(lock_key)
1065 .fetch_one(&mut *connection)
1066 .await
1067 .map_err(CoordinationError::DatabaseError)?;
1068
1069 let acquired: bool = row.get(0);
1070
1071 if acquired {
1072 Ok(CoordinationGuard {
1073 lock_key,
1074 connection: Some(connection),
1075 })
1076 } else {
1077 Err(CoordinationError::LeadershipNotAcquired {
1079 subscription_name: subscription_name.to_string(),
1080 })
1081 }
1082}
1083
1084impl ProjectorCoordinator for PostgresProjectorCoordinator {
1085 type Error = CoordinationError;
1086 type Guard = CoordinationGuard;
1087
1088 async fn try_acquire(&self, subscription_name: &str) -> Result<Self::Guard, Self::Error> {
1089 try_acquire_advisory_lock(&self.pool, subscription_name).await
1090 }
1091}
1092
1093impl ProjectorCoordinator for PostgresEventStore {
1094 type Error = CoordinationError;
1095 type Guard = CoordinationGuard;
1096
1097 async fn try_acquire(&self, subscription_name: &str) -> Result<Self::Guard, Self::Error> {
1098 try_acquire_advisory_lock(&self.pool, subscription_name).await
1099 }
1100}
1101
1102#[cfg(test)]
1103#[path = "lib.test.rs"]
1104mod tests;