1use std::{collections::HashMap, time::Duration};
26
27use eventcore_types::{
28 CheckpointStore, CommandStateReplayCheckpoint, CommandStateSnapshot, CommandStateSnapshotId,
29 Event, EventFilter, EventPage, EventReader, EventStore, EventStoreError, EventStream,
30 EventStreamSlice, Operation, ProjectorCoordinator, StreamId, StreamPosition, StreamVersion,
31 StreamWrites,
32};
33use futures::StreamExt;
34use nutype::nutype;
35use serde_json::value::RawValue;
36use serde_json::{Value, json};
37use sqlx::types::Json;
38use sqlx::{Pool, Postgres, QueryBuilder, Row, postgres::PgPoolOptions, query};
39use thiserror::Error;
40use tracing::{error, info, instrument, warn};
41use uuid::Uuid;
42
43#[derive(Debug, Error)]
45pub enum PostgresEventStoreError {
46 #[error("failed to create postgres connection pool")]
48 ConnectionFailed(#[source] sqlx::Error),
49}
50
51#[nutype(derive(Debug, Clone, Copy, PartialEq, Eq, Display, AsRef, Into))]
70pub struct MaxConnections(std::num::NonZeroU32);
71
72#[derive(Debug, Clone)]
74pub struct PostgresConfig {
75 pub max_connections: MaxConnections,
77 pub acquire_timeout: Duration,
79 pub idle_timeout: Duration,
81}
82
83impl Default for PostgresConfig {
84 fn default() -> Self {
85 const DEFAULT_MAX_CONNECTIONS: std::num::NonZeroU32 = match std::num::NonZeroU32::new(10) {
86 Some(v) => v,
87 None => unreachable!(),
88 };
89
90 Self {
91 max_connections: MaxConnections::new(DEFAULT_MAX_CONNECTIONS),
92 acquire_timeout: Duration::from_secs(30),
93 idle_timeout: Duration::from_secs(600), }
95 }
96}
97
98#[derive(Debug, Clone)]
106pub struct PostgresEventStore {
107 pool: Pool<Postgres>,
108}
109
110impl PostgresEventStore {
111 pub async fn new<S: Into<String>>(
113 connection_string: S,
114 ) -> Result<Self, PostgresEventStoreError> {
115 Self::with_config(connection_string, PostgresConfig::default()).await
116 }
117
118 pub async fn with_config<S: Into<String>>(
120 connection_string: S,
121 config: PostgresConfig,
122 ) -> Result<Self, PostgresEventStoreError> {
123 let connection_string = connection_string.into();
124 let max_connections: std::num::NonZeroU32 = config.max_connections.into();
125 let pool = PgPoolOptions::new()
126 .max_connections(max_connections.get())
127 .acquire_timeout(config.acquire_timeout)
128 .idle_timeout(config.idle_timeout)
129 .connect(&connection_string)
130 .await
131 .map_err(PostgresEventStoreError::ConnectionFailed)?;
132 Ok(Self { pool })
133 }
134
135 pub fn from_pool(pool: Pool<Postgres>) -> Self {
140 Self { pool }
141 }
142
143 #[cfg_attr(test, mutants::skip)] pub async fn ping(&self) {
150 let _ = query("SELECT 1")
151 .execute(&self.pool)
152 .await
153 .expect("postgres ping failed");
154 }
155
156 #[cfg_attr(test, mutants::skip)] pub async fn migrate(&self) {
166 sqlx::migrate!("./migrations")
167 .run(&self.pool)
168 .await
169 .expect("postgres migration failed");
170 }
171}
172
173impl EventStore for PostgresEventStore {
174 #[instrument(name = "postgres.read_stream", skip(self))]
175 async fn read_stream<E: Event>(
176 &self,
177 stream_id: StreamId,
178 ) -> Result<EventStream<E>, EventStoreError> {
179 info!(
180 stream = %stream_id,
181 "[postgres.read_stream] reading events from postgres"
182 );
183
184 let pool = self.pool.clone();
190
191 let stream = async_stream::stream! {
192 let mut rows = query(
193 "SELECT event_data FROM eventcore_events WHERE stream_id = $1 ORDER BY stream_version ASC",
194 )
195 .bind(stream_id.as_ref())
196 .fetch(&pool);
197
198 while let Some(row) = rows.next().await {
199 let row = match row {
200 Ok(row) => row,
201 Err(error) => {
202 yield Err(map_sqlx_error(error, Operation::ReadStream));
203 break;
204 }
205 };
206
207 let payload: Value = match row.try_get("event_data") {
208 Ok(payload) => payload,
209 Err(error) => {
210 yield Err(map_sqlx_error(error, Operation::ReadStream));
211 break;
212 }
213 };
214
215 match serde_json::from_value::<E>(payload) {
219 Ok(event) => yield Ok(event),
220 Err(error) => {
221 yield Err(EventStoreError::DeserializationFailed {
222 stream_id: stream_id.clone(),
223 detail: error.to_string(),
224 });
225 break;
226 }
227 }
228 }
229 };
230
231 Ok(EventStream::new(stream))
232 }
233
234 #[instrument(name = "postgres.read_stream_after", skip(self))]
235 async fn read_stream_after<E: Event>(
236 &self,
237 stream_id: StreamId,
238 exclusive_version: StreamVersion,
239 ) -> Result<EventStream<E>, EventStoreError> {
240 let pool = self.pool.clone();
241 let version: usize = exclusive_version.into();
242
243 let stream = async_stream::stream! {
244 let mut rows = query(
245 "SELECT event_data FROM eventcore_events \
246 WHERE stream_id = $1 AND stream_version > $2 \
247 ORDER BY stream_version ASC",
248 )
249 .bind(stream_id.as_ref())
250 .bind(version as i64)
251 .fetch(&pool);
252
253 while let Some(row) = rows.next().await {
254 let row = match row {
255 Ok(row) => row,
256 Err(error) => {
257 yield Err(map_sqlx_error(error, Operation::ReadStream));
258 break;
259 }
260 };
261
262 let payload: Value = match row.try_get("event_data") {
263 Ok(payload) => payload,
264 Err(error) => {
265 yield Err(map_sqlx_error(error, Operation::ReadStream));
266 break;
267 }
268 };
269
270 match serde_json::from_value::<E>(payload) {
271 Ok(event) => yield Ok(event),
272 Err(error) => {
273 yield Err(EventStoreError::DeserializationFailed {
274 stream_id: stream_id.clone(),
275 detail: error.to_string(),
276 });
277 break;
278 }
279 }
280 }
281 };
282
283 Ok(EventStream::new(stream))
284 }
285
286 #[instrument(name = "postgres.append_events", skip(self, writes))]
287 async fn append_events(
288 &self,
289 writes: StreamWrites,
290 ) -> Result<EventStreamSlice, EventStoreError> {
291 let expected_versions = writes.expected_versions().clone();
292 let entries = writes.into_entries();
293
294 if entries.is_empty() {
295 return Ok(EventStreamSlice);
296 }
297
298 info!(
299 stream_count = expected_versions.len(),
300 event_count = entries.len(),
301 "[postgres.append_events] appending events to postgres"
302 );
303
304 let expected_versions_json: Value = expected_versions
306 .iter()
307 .map(|(stream_id, version)| {
308 (stream_id.as_ref().to_string(), json!(version.into_inner()))
309 })
310 .collect();
311
312 let mut tx = self
313 .pool
314 .begin()
315 .await
316 .map_err(|error| map_sqlx_error(error, Operation::BeginTransaction))?;
317
318 validate_expected_versions(&mut tx, &expected_versions).await?;
319
320 let _ = query("SELECT set_config('eventcore.expected_versions', $1, true)")
322 .bind(expected_versions_json.to_string())
323 .execute(&mut *tx)
324 .await
325 .map_err(|error| map_sqlx_error(error, Operation::SetExpectedVersions))?;
326
327 let rows: Vec<(StreamId, &'static str, Box<RawValue>)> = entries
337 .into_iter()
338 .map(|entry| (entry.stream_id, entry.event_type, entry.event_data))
339 .collect();
340
341 const MAX_EVENTS_PER_INSERT: usize = 1000;
342 for chunk in rows.chunks(MAX_EVENTS_PER_INSERT) {
343 let mut builder = QueryBuilder::<Postgres>::new(
344 "INSERT INTO eventcore_events (event_id, stream_id, event_type, event_data, metadata) ",
345 );
346 let _ = builder.push_values(chunk, |mut row, (stream_id, event_type, event_data)| {
347 let _ = row
348 .push_bind(Uuid::now_v7())
349 .push_bind(stream_id.as_ref())
350 .push_bind(*event_type)
351 .push_bind(Json(event_data))
352 .push_bind(Json(json!({})));
353 });
354 let _ = builder
355 .build()
356 .execute(&mut *tx)
357 .await
358 .map_err(|error| map_sqlx_error(error, Operation::AppendEvents))?;
359 }
360
361 tx.commit()
362 .await
363 .map_err(|error| map_sqlx_error(error, Operation::CommitTransaction))?;
364
365 Ok(EventStreamSlice)
366 }
367
368 async fn load_command_state_snapshot(
369 &self,
370 snapshot_id: CommandStateSnapshotId,
371 ) -> Result<Option<CommandStateSnapshot>, EventStoreError> {
372 let row = query(
373 "SELECT state, stream_versions, replay_checkpoints \
374 FROM eventcore_command_state_snapshots \
375 WHERE snapshot_id = $1",
376 )
377 .bind(snapshot_id.as_ref())
378 .fetch_optional(&self.pool)
379 .await
380 .map_err(|error| map_sqlx_error(error, Operation::ReadStream))?;
381
382 row.map(|row| {
383 let state: Json<Value> = row
384 .try_get("state")
385 .map_err(|error| map_sqlx_error(error, Operation::ReadStream))?;
386 let stream_versions: Json<HashMap<StreamId, StreamVersion>> = row
387 .try_get("stream_versions")
388 .map_err(|error| map_sqlx_error(error, Operation::ReadStream))?;
389 let replay_checkpoints: Json<Vec<CommandStateReplayCheckpoint>> = row
390 .try_get("replay_checkpoints")
391 .map_err(|error| map_sqlx_error(error, Operation::ReadStream))?;
392
393 Ok(CommandStateSnapshot::new(state.0, stream_versions.0)
394 .with_replay_checkpoints(replay_checkpoints.0))
395 })
396 .transpose()
397 }
398
399 async fn save_command_state_snapshot(
400 &self,
401 snapshot_id: CommandStateSnapshotId,
402 snapshot: CommandStateSnapshot,
403 ) -> Result<(), EventStoreError> {
404 let _ = query(
408 "INSERT INTO eventcore_command_state_snapshots \
409 (snapshot_id, state, stream_versions, replay_checkpoints, updated_at) \
410 VALUES ($1, $2, $3, $4, NOW()) \
411 ON CONFLICT (snapshot_id) DO UPDATE \
412 SET state = EXCLUDED.state, \
413 stream_versions = EXCLUDED.stream_versions, \
414 replay_checkpoints = EXCLUDED.replay_checkpoints, \
415 updated_at = NOW() \
416 WHERE NOT EXISTS ( \
417 SELECT 1 \
418 FROM jsonb_each_text(eventcore_command_state_snapshots.stream_versions) \
419 AS current(stream_id, stream_version) \
420 WHERE NOT (EXCLUDED.stream_versions ? current.stream_id) \
421 OR (EXCLUDED.stream_versions ->> current.stream_id)::BIGINT \
422 < current.stream_version::BIGINT \
423 )",
424 )
425 .bind(snapshot_id.as_ref())
426 .bind(Json(snapshot.state))
427 .bind(Json(snapshot.stream_versions))
428 .bind(Json(snapshot.replay_checkpoints))
429 .execute(&self.pool)
430 .await
431 .map_err(|error| map_sqlx_error(error, Operation::AppendEvents))?;
432
433 Ok(())
434 }
435}
436
437impl CheckpointStore for PostgresEventStore {
438 type Error = PostgresCheckpointError;
439
440 async fn load(&self, name: &str) -> Result<Option<StreamPosition>, Self::Error> {
441 let row = query("SELECT last_position FROM eventcore_subscription_versions WHERE subscription_name = $1")
442 .bind(name)
443 .fetch_optional(&self.pool)
444 .await
445 .map_err(PostgresCheckpointError::DatabaseError)?;
446
447 match row {
448 Some(row) => {
449 let position: Uuid = row.get("last_position");
450 Ok(Some(StreamPosition::new(position)))
451 }
452 None => Ok(None),
453 }
454 }
455
456 async fn save(&self, name: &str, position: StreamPosition) -> Result<(), Self::Error> {
457 let position_uuid: Uuid = position.into_inner();
458 let _ = query(
459 "INSERT INTO eventcore_subscription_versions (subscription_name, last_position, updated_at)
460 VALUES ($1, $2, NOW())
461 ON CONFLICT (subscription_name) DO UPDATE SET last_position = $2, updated_at = NOW()",
462 )
463 .bind(name)
464 .bind(position_uuid)
465 .execute(&self.pool)
466 .await
467 .map_err(PostgresCheckpointError::DatabaseError)?;
468
469 Ok(())
470 }
471}
472
473async fn validate_expected_versions(
483 tx: &mut sqlx::Transaction<'_, Postgres>,
484 expected_versions: &HashMap<StreamId, StreamVersion>,
485) -> Result<(), EventStoreError> {
486 let mut expected_streams: Vec<_> = expected_versions.iter().collect();
487 expected_streams.sort_unstable_by(|(left, _), (right, _)| left.as_ref().cmp(right.as_ref()));
488
489 for (stream_id, _) in &expected_streams {
490 let _ = query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))")
491 .bind(stream_id.as_ref())
492 .execute(&mut **tx)
493 .await
494 .map_err(|error| map_sqlx_error(error, Operation::AppendEvents))?;
495 }
496
497 for (stream_id, expected_version) in expected_streams {
498 let current_version: i64 = sqlx::query_scalar(
499 "SELECT COALESCE(MAX(stream_version), 0) FROM eventcore_events WHERE stream_id = $1",
500 )
501 .bind(stream_id.as_ref())
502 .fetch_one(&mut **tx)
503 .await
504 .map_err(|error| map_sqlx_error(error, Operation::AppendEvents))?;
505 let current_version =
506 usize::try_from(current_version).map_err(|_| EventStoreError::StoreFailure {
507 operation: Operation::AppendEvents,
508 })?;
509 let actual = StreamVersion::new(current_version);
510
511 if actual != *expected_version {
512 warn!(
513 stream = %stream_id,
514 expected = expected_version.into_inner(),
515 actual = actual.into_inner(),
516 "[postgres.version_conflict] optimistic concurrency check failed"
517 );
518 return Err(EventStoreError::VersionConflict {
519 stream_id: stream_id.clone(),
520 expected: *expected_version,
521 actual,
522 });
523 }
524 }
525
526 Ok(())
527}
528
529impl EventReader for PostgresEventStore {
530 type Error = EventStoreError;
531
532 async fn read_events<E: Event>(
533 &self,
534 filter: EventFilter,
535 page: EventPage,
536 ) -> Result<Vec<(E, StreamPosition)>, Self::Error> {
537 let after_event_id: Option<Uuid> = page.after_position().map(|p| p.into_inner());
540 let limit: i64 = page.limit().into_inner() as i64;
541
542 let type_filter = filter.event_type().unwrap_or_else(|| E::event_type_name());
546
547 let pattern_regex = filter
552 .stream_pattern()
553 .map(|p| glob_to_anchored_regex(p.as_ref()));
554
555 let mut builder = QueryBuilder::<Postgres>::new(
559 "SELECT event_id, event_data, stream_id FROM eventcore_events WHERE event_type = ",
560 );
561 let _ = builder.push_bind(type_filter);
562
563 if let Some(after_id) = after_event_id {
564 let _ = builder.push(" AND event_id > ").push_bind(after_id);
565 }
566
567 if let Some(prefix) = filter.stream_prefix() {
568 let _ = builder
569 .push(" AND stream_id LIKE ")
570 .push_bind(prefix.as_ref().to_string())
571 .push(" || '%'");
572 } else if let Some(regex) = pattern_regex {
573 let _ = builder.push(" AND stream_id ~ ").push_bind(regex);
574 }
575
576 let _ = builder.push(" ORDER BY event_id LIMIT ").push_bind(limit);
577
578 let rows = builder
579 .build()
580 .fetch_all(&self.pool)
581 .await
582 .map_err(|error| map_sqlx_error(error, Operation::ReadStream))?;
583
584 let events: Vec<(E, StreamPosition)> = rows
585 .into_iter()
586 .filter_map(|row| {
587 let event_data: Json<Value> = row.get("event_data");
588 let event_id: Uuid = row.get("event_id");
589 serde_json::from_value::<E>(event_data.0)
590 .ok()
591 .map(|e| (e, StreamPosition::new(event_id)))
592 })
593 .collect();
594
595 Ok(events)
596 }
597}
598
599fn glob_to_anchored_regex(glob: &str) -> String {
615 let mut regex = String::with_capacity(glob.len() + 2);
616 regex.push('^');
617
618 let mut chars = glob.chars().peekable();
619 while let Some(c) = chars.next() {
620 match c {
621 '*' => regex.push_str(".*"),
622 '?' => regex.push('.'),
623 '[' => {
624 let mut class = String::new();
626 let mut closed = false;
627 if matches!(chars.peek(), Some('!')) {
628 let _ = chars.next();
629 class.push('^');
630 }
631 for inner in chars.by_ref() {
632 if inner == ']' {
633 closed = true;
634 break;
635 }
636 class.push(inner);
637 }
638 if closed {
639 regex.push('[');
640 regex.push_str(&class);
641 regex.push(']');
642 } else {
643 regex.push_str(®ex_escape("["));
646 regex.push_str(®ex_escape(&class));
647 }
648 }
649 other => regex.push_str(®ex_escape(&other.to_string())),
650 }
651 }
652
653 regex.push('$');
654 regex
655}
656
657fn regex_escape(literal: &str) -> String {
660 const METACHARACTERS: &[char] = &[
661 '.', '^', '$', '*', '+', '?', '(', ')', '[', ']', '{', '}', '|', '\\',
662 ];
663 let mut escaped = String::with_capacity(literal.len());
664 for c in literal.chars() {
665 if METACHARACTERS.contains(&c) {
666 escaped.push('\\');
667 }
668 escaped.push(c);
669 }
670 escaped
671}
672
673fn map_sqlx_error(error: sqlx::Error, operation: Operation) -> EventStoreError {
674 if let sqlx::Error::Database(db_error) = &error {
675 let code = db_error.code();
676 let code_str = code.as_deref();
677 if code_str == Some("P0001") || code_str == Some("23505") {
680 warn!(
681 error = %db_error,
682 "[postgres.version_conflict] optimistic concurrency check failed"
683 );
684 return parse_version_conflict_from_db_error(db_error.message());
685 }
686 }
687
688 error!(
689 error = %error,
690 operation = %operation,
691 "[postgres.database_error] database operation failed"
692 );
693 EventStoreError::StoreFailure { operation }
694}
695
696fn parse_version_conflict_from_db_error(message: &str) -> EventStoreError {
704 if let Some(parsed) = try_parse_conflict_message(message) {
706 return parsed;
707 }
708
709 let fallback_stream_id =
712 StreamId::try_new("unknown-conflict-stream").expect("static stream id is valid");
713 EventStoreError::VersionConflict {
714 stream_id: fallback_stream_id,
715 expected: StreamVersion::new(0),
716 actual: StreamVersion::new(0),
717 }
718}
719
720fn try_parse_conflict_message(message: &str) -> Option<EventStoreError> {
721 let rest = message.strip_prefix("version_conflict: stream \"")?;
722 let stream_end = rest.find('"')?;
723 let stream_id_str = &rest[..stream_end];
724 let after_stream = &rest[stream_end..];
725
726 let expected_str = after_stream
727 .strip_prefix("\" expected version ")?
728 .split(',')
729 .next()?;
730 let actual_str = after_stream.rsplit("actual ").next()?;
731
732 let expected = expected_str.trim().parse::<usize>().ok()?;
733 let actual = actual_str.trim().parse::<usize>().ok()?;
734 let stream_id = StreamId::try_new(stream_id_str).ok()?;
735
736 Some(EventStoreError::VersionConflict {
737 stream_id,
738 expected: StreamVersion::new(expected),
739 actual: StreamVersion::new(actual),
740 })
741}
742
743#[derive(Debug, Error)]
745pub enum PostgresCheckpointError {
746 #[error("failed to create postgres connection pool")]
748 ConnectionFailed(#[source] sqlx::Error),
749
750 #[error("database operation failed: {0}")]
752 DatabaseError(#[source] sqlx::Error),
753}
754
755#[derive(Debug, Clone)]
768pub struct PostgresCheckpointStore {
769 pool: Pool<Postgres>,
770}
771
772impl PostgresCheckpointStore {
773 pub async fn new<S: Into<String>>(
775 connection_string: S,
776 ) -> Result<Self, PostgresCheckpointError> {
777 Self::with_config(connection_string, PostgresConfig::default()).await
778 }
779
780 pub async fn with_config<S: Into<String>>(
782 connection_string: S,
783 config: PostgresConfig,
784 ) -> Result<Self, PostgresCheckpointError> {
785 let connection_string = connection_string.into();
786 let max_connections: std::num::NonZeroU32 = config.max_connections.into();
787 let pool = PgPoolOptions::new()
788 .max_connections(max_connections.get())
789 .acquire_timeout(config.acquire_timeout)
790 .idle_timeout(config.idle_timeout)
791 .connect(&connection_string)
792 .await
793 .map_err(PostgresCheckpointError::ConnectionFailed)?;
794
795 sqlx::migrate!("./migrations")
797 .run(&pool)
798 .await
799 .map_err(|e| {
800 PostgresCheckpointError::DatabaseError(sqlx::Error::Migrate(Box::new(e)))
801 })?;
802
803 Ok(Self { pool })
804 }
805
806 pub fn from_pool(pool: Pool<Postgres>) -> Self {
811 Self { pool }
812 }
813}
814
815impl CheckpointStore for PostgresCheckpointStore {
816 type Error = PostgresCheckpointError;
817
818 async fn load(&self, name: &str) -> Result<Option<StreamPosition>, Self::Error> {
819 let row = query("SELECT last_position FROM eventcore_subscription_versions WHERE subscription_name = $1")
820 .bind(name)
821 .fetch_optional(&self.pool)
822 .await
823 .map_err(PostgresCheckpointError::DatabaseError)?;
824
825 match row {
826 Some(row) => {
827 let position: Uuid = row.get("last_position");
828 Ok(Some(StreamPosition::new(position)))
829 }
830 None => Ok(None),
831 }
832 }
833
834 async fn save(&self, name: &str, position: StreamPosition) -> Result<(), Self::Error> {
835 let position_uuid: Uuid = position.into_inner();
836 let _ = query(
837 "INSERT INTO eventcore_subscription_versions (subscription_name, last_position, updated_at)
838 VALUES ($1, $2, NOW())
839 ON CONFLICT (subscription_name) DO UPDATE SET last_position = $2, updated_at = NOW()",
840 )
841 .bind(name)
842 .bind(position_uuid)
843 .execute(&self.pool)
844 .await
845 .map_err(PostgresCheckpointError::DatabaseError)?;
846
847 Ok(())
848 }
849}
850
851#[derive(Debug, Error)]
857pub enum CoordinationError {
858 #[error(
860 "leadership not acquired for subscription '{subscription_name}': another instance holds the lock"
861 )]
862 LeadershipNotAcquired { subscription_name: String },
863
864 #[error("database operation failed: {0}")]
866 DatabaseError(#[source] sqlx::Error),
867}
868
869pub struct CoordinationGuard {
899 lock_key: i64,
900 connection: Option<sqlx::pool::PoolConnection<Postgres>>,
903}
904
905impl std::fmt::Debug for CoordinationGuard {
906 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
907 f.debug_struct("CoordinationGuard")
908 .field("lock_key", &self.lock_key)
909 .finish_non_exhaustive()
910 }
911}
912
913impl Drop for CoordinationGuard {
914 fn drop(&mut self) {
915 if let Some(mut connection) = self.connection.take() {
917 let lock_key = self.lock_key;
918
919 let handle = tokio::runtime::Handle::current();
922 let is_multi_thread =
923 handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread;
924
925 if is_multi_thread {
926 tokio::task::block_in_place(|| {
928 handle.block_on(async {
929 if let Err(e) = query("SELECT pg_advisory_unlock($1)")
931 .bind(lock_key)
932 .execute(&mut *connection)
933 .await
934 {
935 warn!(
936 lock_key = lock_key,
937 error = %e,
938 "failed to release advisory lock on drop"
939 );
940 }
941 });
943 });
944 } else {
945 drop(tokio::spawn(async move {
950 if let Err(e) = query("SELECT pg_advisory_unlock($1)")
951 .bind(lock_key)
952 .execute(&mut *connection)
953 .await
954 {
955 warn!(
956 lock_key = lock_key,
957 error = %e,
958 "failed to release advisory lock on drop (async)"
959 );
960 }
961 }));
962 }
963 }
964 }
965}
966
967#[derive(Debug, Clone)]
973pub struct PostgresProjectorCoordinator {
974 pool: Pool<Postgres>,
975}
976
977impl PostgresProjectorCoordinator {
978 pub async fn new<S: Into<String>>(connection_string: S) -> Result<Self, CoordinationError> {
980 Self::with_config(connection_string, PostgresConfig::default()).await
981 }
982
983 pub async fn with_config<S: Into<String>>(
985 connection_string: S,
986 config: PostgresConfig,
987 ) -> Result<Self, CoordinationError> {
988 let connection_string = connection_string.into();
989 let max_connections: std::num::NonZeroU32 = config.max_connections.into();
990 let pool = PgPoolOptions::new()
991 .max_connections(max_connections.get())
992 .acquire_timeout(config.acquire_timeout)
993 .idle_timeout(config.idle_timeout)
994 .connect(&connection_string)
995 .await
996 .map_err(CoordinationError::DatabaseError)?;
997
998 Ok(Self { pool })
999 }
1000
1001 pub fn from_pool(pool: Pool<Postgres>) -> Self {
1003 Self { pool }
1004 }
1005}
1006
1007fn advisory_lock_key(subscription_name: &str) -> i64 {
1012 const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
1013 const FNV_PRIME: u64 = 0x00000100000001B3;
1014
1015 let mut hash = FNV_OFFSET_BASIS;
1016 for byte in subscription_name.as_bytes() {
1017 hash ^= *byte as u64;
1018 hash = hash.wrapping_mul(FNV_PRIME);
1019 }
1020 hash as i64
1021}
1022
1023async fn try_acquire_advisory_lock(
1026 pool: &Pool<Postgres>,
1027 subscription_name: &str,
1028) -> Result<CoordinationGuard, CoordinationError> {
1029 let lock_key = advisory_lock_key(subscription_name);
1030
1031 let mut connection = pool
1035 .acquire()
1036 .await
1037 .map_err(CoordinationError::DatabaseError)?;
1038
1039 let row = query("SELECT pg_try_advisory_lock($1)")
1041 .bind(lock_key)
1042 .fetch_one(&mut *connection)
1043 .await
1044 .map_err(CoordinationError::DatabaseError)?;
1045
1046 let acquired: bool = row.get(0);
1047
1048 if acquired {
1049 Ok(CoordinationGuard {
1050 lock_key,
1051 connection: Some(connection),
1052 })
1053 } else {
1054 Err(CoordinationError::LeadershipNotAcquired {
1056 subscription_name: subscription_name.to_string(),
1057 })
1058 }
1059}
1060
1061impl ProjectorCoordinator for PostgresProjectorCoordinator {
1062 type Error = CoordinationError;
1063 type Guard = CoordinationGuard;
1064
1065 async fn try_acquire(&self, subscription_name: &str) -> Result<Self::Guard, Self::Error> {
1066 try_acquire_advisory_lock(&self.pool, subscription_name).await
1067 }
1068}
1069
1070impl ProjectorCoordinator for PostgresEventStore {
1071 type Error = CoordinationError;
1072 type Guard = CoordinationGuard;
1073
1074 async fn try_acquire(&self, subscription_name: &str) -> Result<Self::Guard, Self::Error> {
1075 try_acquire_advisory_lock(&self.pool, subscription_name).await
1076 }
1077}
1078
1079#[cfg(test)]
1080#[path = "lib.test.rs"]
1081mod tests;