1use std::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 let _ = query("SELECT set_config('eventcore.expected_versions', $1, true)")
320 .bind(expected_versions_json.to_string())
321 .execute(&mut *tx)
322 .await
323 .map_err(|error| map_sqlx_error(error, Operation::SetExpectedVersions))?;
324
325 let rows: Vec<(StreamId, &'static str, Box<RawValue>)> = entries
335 .into_iter()
336 .map(|entry| (entry.stream_id, entry.event_type, entry.event_data))
337 .collect();
338
339 const MAX_EVENTS_PER_INSERT: usize = 1000;
340 for chunk in rows.chunks(MAX_EVENTS_PER_INSERT) {
341 let mut builder = QueryBuilder::<Postgres>::new(
342 "INSERT INTO eventcore_events (event_id, stream_id, event_type, event_data, metadata) ",
343 );
344 let _ = builder.push_values(chunk, |mut row, (stream_id, event_type, event_data)| {
345 let _ = row
346 .push_bind(Uuid::now_v7())
347 .push_bind(stream_id.as_ref())
348 .push_bind(*event_type)
349 .push_bind(Json(event_data))
350 .push_bind(Json(json!({})));
351 });
352 let _ = builder
353 .build()
354 .execute(&mut *tx)
355 .await
356 .map_err(|error| map_sqlx_error(error, Operation::AppendEvents))?;
357 }
358
359 tx.commit()
360 .await
361 .map_err(|error| map_sqlx_error(error, Operation::CommitTransaction))?;
362
363 Ok(EventStreamSlice)
364 }
365
366 async fn load_command_state_snapshot(
367 &self,
368 snapshot_id: CommandStateSnapshotId,
369 ) -> Result<Option<CommandStateSnapshot>, EventStoreError> {
370 let row = query(
371 "SELECT state, stream_versions, replay_checkpoints \
372 FROM eventcore_command_state_snapshots \
373 WHERE snapshot_id = $1",
374 )
375 .bind(snapshot_id.as_ref())
376 .fetch_optional(&self.pool)
377 .await
378 .map_err(|error| map_sqlx_error(error, Operation::ReadStream))?;
379
380 row.map(|row| {
381 let state: Json<Value> = row
382 .try_get("state")
383 .map_err(|error| map_sqlx_error(error, Operation::ReadStream))?;
384 let stream_versions: Json<std::collections::HashMap<StreamId, StreamVersion>> = row
385 .try_get("stream_versions")
386 .map_err(|error| map_sqlx_error(error, Operation::ReadStream))?;
387 let replay_checkpoints: Json<Vec<CommandStateReplayCheckpoint>> = row
388 .try_get("replay_checkpoints")
389 .map_err(|error| map_sqlx_error(error, Operation::ReadStream))?;
390
391 Ok(CommandStateSnapshot::new(state.0, stream_versions.0)
392 .with_replay_checkpoints(replay_checkpoints.0))
393 })
394 .transpose()
395 }
396
397 async fn save_command_state_snapshot(
398 &self,
399 snapshot_id: CommandStateSnapshotId,
400 snapshot: CommandStateSnapshot,
401 ) -> Result<(), EventStoreError> {
402 let _ = query(
406 "INSERT INTO eventcore_command_state_snapshots \
407 (snapshot_id, state, stream_versions, replay_checkpoints, updated_at) \
408 VALUES ($1, $2, $3, $4, NOW()) \
409 ON CONFLICT (snapshot_id) DO UPDATE \
410 SET state = EXCLUDED.state, \
411 stream_versions = EXCLUDED.stream_versions, \
412 replay_checkpoints = EXCLUDED.replay_checkpoints, \
413 updated_at = NOW() \
414 WHERE NOT EXISTS ( \
415 SELECT 1 \
416 FROM jsonb_each_text(eventcore_command_state_snapshots.stream_versions) \
417 AS current(stream_id, stream_version) \
418 WHERE NOT (EXCLUDED.stream_versions ? current.stream_id) \
419 OR (EXCLUDED.stream_versions ->> current.stream_id)::BIGINT \
420 < current.stream_version::BIGINT \
421 )",
422 )
423 .bind(snapshot_id.as_ref())
424 .bind(Json(snapshot.state))
425 .bind(Json(snapshot.stream_versions))
426 .bind(Json(snapshot.replay_checkpoints))
427 .execute(&self.pool)
428 .await
429 .map_err(|error| map_sqlx_error(error, Operation::AppendEvents))?;
430
431 Ok(())
432 }
433}
434
435impl CheckpointStore for PostgresEventStore {
436 type Error = PostgresCheckpointError;
437
438 async fn load(&self, name: &str) -> Result<Option<StreamPosition>, Self::Error> {
439 let row = query("SELECT last_position FROM eventcore_subscription_versions WHERE subscription_name = $1")
440 .bind(name)
441 .fetch_optional(&self.pool)
442 .await
443 .map_err(PostgresCheckpointError::DatabaseError)?;
444
445 match row {
446 Some(row) => {
447 let position: Uuid = row.get("last_position");
448 Ok(Some(StreamPosition::new(position)))
449 }
450 None => Ok(None),
451 }
452 }
453
454 async fn save(&self, name: &str, position: StreamPosition) -> Result<(), Self::Error> {
455 let position_uuid: Uuid = position.into_inner();
456 let _ = query(
457 "INSERT INTO eventcore_subscription_versions (subscription_name, last_position, updated_at)
458 VALUES ($1, $2, NOW())
459 ON CONFLICT (subscription_name) DO UPDATE SET last_position = $2, updated_at = NOW()",
460 )
461 .bind(name)
462 .bind(position_uuid)
463 .execute(&self.pool)
464 .await
465 .map_err(PostgresCheckpointError::DatabaseError)?;
466
467 Ok(())
468 }
469}
470
471impl EventReader for PostgresEventStore {
472 type Error = EventStoreError;
473
474 async fn read_events<E: Event>(
475 &self,
476 filter: EventFilter,
477 page: EventPage,
478 ) -> Result<Vec<(E, StreamPosition)>, Self::Error> {
479 let after_event_id: Option<Uuid> = page.after_position().map(|p| p.into_inner());
482 let limit: i64 = page.limit().into_inner() as i64;
483
484 let type_filter = filter.event_type().unwrap_or_else(|| E::event_type_name());
488
489 let pattern_regex = filter
494 .stream_pattern()
495 .map(|p| glob_to_anchored_regex(p.as_ref()));
496
497 let mut builder = QueryBuilder::<Postgres>::new(
501 "SELECT event_id, event_data, stream_id FROM eventcore_events WHERE event_type = ",
502 );
503 let _ = builder.push_bind(type_filter);
504
505 if let Some(after_id) = after_event_id {
506 let _ = builder.push(" AND event_id > ").push_bind(after_id);
507 }
508
509 if let Some(prefix) = filter.stream_prefix() {
510 let _ = builder
511 .push(" AND stream_id LIKE ")
512 .push_bind(prefix.as_ref().to_string())
513 .push(" || '%'");
514 } else if let Some(regex) = pattern_regex {
515 let _ = builder.push(" AND stream_id ~ ").push_bind(regex);
516 }
517
518 let _ = builder.push(" ORDER BY event_id LIMIT ").push_bind(limit);
519
520 let rows = builder
521 .build()
522 .fetch_all(&self.pool)
523 .await
524 .map_err(|error| map_sqlx_error(error, Operation::ReadStream))?;
525
526 let events: Vec<(E, StreamPosition)> = rows
527 .into_iter()
528 .filter_map(|row| {
529 let event_data: Json<Value> = row.get("event_data");
530 let event_id: Uuid = row.get("event_id");
531 serde_json::from_value::<E>(event_data.0)
532 .ok()
533 .map(|e| (e, StreamPosition::new(event_id)))
534 })
535 .collect();
536
537 Ok(events)
538 }
539}
540
541fn glob_to_anchored_regex(glob: &str) -> String {
557 let mut regex = String::with_capacity(glob.len() + 2);
558 regex.push('^');
559
560 let mut chars = glob.chars().peekable();
561 while let Some(c) = chars.next() {
562 match c {
563 '*' => regex.push_str(".*"),
564 '?' => regex.push('.'),
565 '[' => {
566 let mut class = String::new();
568 let mut closed = false;
569 if matches!(chars.peek(), Some('!')) {
570 let _ = chars.next();
571 class.push('^');
572 }
573 for inner in chars.by_ref() {
574 if inner == ']' {
575 closed = true;
576 break;
577 }
578 class.push(inner);
579 }
580 if closed {
581 regex.push('[');
582 regex.push_str(&class);
583 regex.push(']');
584 } else {
585 regex.push_str(®ex_escape("["));
588 regex.push_str(®ex_escape(&class));
589 }
590 }
591 other => regex.push_str(®ex_escape(&other.to_string())),
592 }
593 }
594
595 regex.push('$');
596 regex
597}
598
599fn regex_escape(literal: &str) -> String {
602 const METACHARACTERS: &[char] = &[
603 '.', '^', '$', '*', '+', '?', '(', ')', '[', ']', '{', '}', '|', '\\',
604 ];
605 let mut escaped = String::with_capacity(literal.len());
606 for c in literal.chars() {
607 if METACHARACTERS.contains(&c) {
608 escaped.push('\\');
609 }
610 escaped.push(c);
611 }
612 escaped
613}
614
615fn map_sqlx_error(error: sqlx::Error, operation: Operation) -> EventStoreError {
616 if let sqlx::Error::Database(db_error) = &error {
617 let code = db_error.code();
618 let code_str = code.as_deref();
619 if code_str == Some("P0001") || code_str == Some("23505") {
622 warn!(
623 error = %db_error,
624 "[postgres.version_conflict] optimistic concurrency check failed"
625 );
626 return parse_version_conflict_from_db_error(db_error.message());
627 }
628 }
629
630 error!(
631 error = %error,
632 operation = %operation,
633 "[postgres.database_error] database operation failed"
634 );
635 EventStoreError::StoreFailure { operation }
636}
637
638fn parse_version_conflict_from_db_error(message: &str) -> EventStoreError {
646 if let Some(parsed) = try_parse_conflict_message(message) {
648 return parsed;
649 }
650
651 let fallback_stream_id =
654 StreamId::try_new("unknown-conflict-stream").expect("static stream id is valid");
655 EventStoreError::VersionConflict {
656 stream_id: fallback_stream_id,
657 expected: StreamVersion::new(0),
658 actual: StreamVersion::new(0),
659 }
660}
661
662fn try_parse_conflict_message(message: &str) -> Option<EventStoreError> {
663 let rest = message.strip_prefix("version_conflict: stream \"")?;
664 let stream_end = rest.find('"')?;
665 let stream_id_str = &rest[..stream_end];
666 let after_stream = &rest[stream_end..];
667
668 let expected_str = after_stream
669 .strip_prefix("\" expected version ")?
670 .split(',')
671 .next()?;
672 let actual_str = after_stream.rsplit("actual ").next()?;
673
674 let expected = expected_str.trim().parse::<usize>().ok()?;
675 let actual = actual_str.trim().parse::<usize>().ok()?;
676 let stream_id = StreamId::try_new(stream_id_str).ok()?;
677
678 Some(EventStoreError::VersionConflict {
679 stream_id,
680 expected: StreamVersion::new(expected),
681 actual: StreamVersion::new(actual),
682 })
683}
684
685#[derive(Debug, Error)]
687pub enum PostgresCheckpointError {
688 #[error("failed to create postgres connection pool")]
690 ConnectionFailed(#[source] sqlx::Error),
691
692 #[error("database operation failed: {0}")]
694 DatabaseError(#[source] sqlx::Error),
695}
696
697#[derive(Debug, Clone)]
710pub struct PostgresCheckpointStore {
711 pool: Pool<Postgres>,
712}
713
714impl PostgresCheckpointStore {
715 pub async fn new<S: Into<String>>(
717 connection_string: S,
718 ) -> Result<Self, PostgresCheckpointError> {
719 Self::with_config(connection_string, PostgresConfig::default()).await
720 }
721
722 pub async fn with_config<S: Into<String>>(
724 connection_string: S,
725 config: PostgresConfig,
726 ) -> Result<Self, PostgresCheckpointError> {
727 let connection_string = connection_string.into();
728 let max_connections: std::num::NonZeroU32 = config.max_connections.into();
729 let pool = PgPoolOptions::new()
730 .max_connections(max_connections.get())
731 .acquire_timeout(config.acquire_timeout)
732 .idle_timeout(config.idle_timeout)
733 .connect(&connection_string)
734 .await
735 .map_err(PostgresCheckpointError::ConnectionFailed)?;
736
737 sqlx::migrate!("./migrations")
739 .run(&pool)
740 .await
741 .map_err(|e| {
742 PostgresCheckpointError::DatabaseError(sqlx::Error::Migrate(Box::new(e)))
743 })?;
744
745 Ok(Self { pool })
746 }
747
748 pub fn from_pool(pool: Pool<Postgres>) -> Self {
753 Self { pool }
754 }
755}
756
757impl CheckpointStore for PostgresCheckpointStore {
758 type Error = PostgresCheckpointError;
759
760 async fn load(&self, name: &str) -> Result<Option<StreamPosition>, Self::Error> {
761 let row = query("SELECT last_position FROM eventcore_subscription_versions WHERE subscription_name = $1")
762 .bind(name)
763 .fetch_optional(&self.pool)
764 .await
765 .map_err(PostgresCheckpointError::DatabaseError)?;
766
767 match row {
768 Some(row) => {
769 let position: Uuid = row.get("last_position");
770 Ok(Some(StreamPosition::new(position)))
771 }
772 None => Ok(None),
773 }
774 }
775
776 async fn save(&self, name: &str, position: StreamPosition) -> Result<(), Self::Error> {
777 let position_uuid: Uuid = position.into_inner();
778 let _ = query(
779 "INSERT INTO eventcore_subscription_versions (subscription_name, last_position, updated_at)
780 VALUES ($1, $2, NOW())
781 ON CONFLICT (subscription_name) DO UPDATE SET last_position = $2, updated_at = NOW()",
782 )
783 .bind(name)
784 .bind(position_uuid)
785 .execute(&self.pool)
786 .await
787 .map_err(PostgresCheckpointError::DatabaseError)?;
788
789 Ok(())
790 }
791}
792
793#[derive(Debug, Error)]
799pub enum CoordinationError {
800 #[error(
802 "leadership not acquired for subscription '{subscription_name}': another instance holds the lock"
803 )]
804 LeadershipNotAcquired { subscription_name: String },
805
806 #[error("database operation failed: {0}")]
808 DatabaseError(#[source] sqlx::Error),
809}
810
811pub struct CoordinationGuard {
841 lock_key: i64,
842 connection: Option<sqlx::pool::PoolConnection<Postgres>>,
845}
846
847impl std::fmt::Debug for CoordinationGuard {
848 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
849 f.debug_struct("CoordinationGuard")
850 .field("lock_key", &self.lock_key)
851 .finish_non_exhaustive()
852 }
853}
854
855impl Drop for CoordinationGuard {
856 fn drop(&mut self) {
857 if let Some(mut connection) = self.connection.take() {
859 let lock_key = self.lock_key;
860
861 let handle = tokio::runtime::Handle::current();
864 let is_multi_thread =
865 handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread;
866
867 if is_multi_thread {
868 tokio::task::block_in_place(|| {
870 handle.block_on(async {
871 if let Err(e) = query("SELECT pg_advisory_unlock($1)")
873 .bind(lock_key)
874 .execute(&mut *connection)
875 .await
876 {
877 warn!(
878 lock_key = lock_key,
879 error = %e,
880 "failed to release advisory lock on drop"
881 );
882 }
883 });
885 });
886 } else {
887 drop(tokio::spawn(async move {
892 if let Err(e) = query("SELECT pg_advisory_unlock($1)")
893 .bind(lock_key)
894 .execute(&mut *connection)
895 .await
896 {
897 warn!(
898 lock_key = lock_key,
899 error = %e,
900 "failed to release advisory lock on drop (async)"
901 );
902 }
903 }));
904 }
905 }
906 }
907}
908
909#[derive(Debug, Clone)]
915pub struct PostgresProjectorCoordinator {
916 pool: Pool<Postgres>,
917}
918
919impl PostgresProjectorCoordinator {
920 pub async fn new<S: Into<String>>(connection_string: S) -> Result<Self, CoordinationError> {
922 Self::with_config(connection_string, PostgresConfig::default()).await
923 }
924
925 pub async fn with_config<S: Into<String>>(
927 connection_string: S,
928 config: PostgresConfig,
929 ) -> Result<Self, CoordinationError> {
930 let connection_string = connection_string.into();
931 let max_connections: std::num::NonZeroU32 = config.max_connections.into();
932 let pool = PgPoolOptions::new()
933 .max_connections(max_connections.get())
934 .acquire_timeout(config.acquire_timeout)
935 .idle_timeout(config.idle_timeout)
936 .connect(&connection_string)
937 .await
938 .map_err(CoordinationError::DatabaseError)?;
939
940 Ok(Self { pool })
941 }
942
943 pub fn from_pool(pool: Pool<Postgres>) -> Self {
945 Self { pool }
946 }
947}
948
949fn advisory_lock_key(subscription_name: &str) -> i64 {
954 const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
955 const FNV_PRIME: u64 = 0x00000100000001B3;
956
957 let mut hash = FNV_OFFSET_BASIS;
958 for byte in subscription_name.as_bytes() {
959 hash ^= *byte as u64;
960 hash = hash.wrapping_mul(FNV_PRIME);
961 }
962 hash as i64
963}
964
965async fn try_acquire_advisory_lock(
968 pool: &Pool<Postgres>,
969 subscription_name: &str,
970) -> Result<CoordinationGuard, CoordinationError> {
971 let lock_key = advisory_lock_key(subscription_name);
972
973 let mut connection = pool
977 .acquire()
978 .await
979 .map_err(CoordinationError::DatabaseError)?;
980
981 let row = query("SELECT pg_try_advisory_lock($1)")
983 .bind(lock_key)
984 .fetch_one(&mut *connection)
985 .await
986 .map_err(CoordinationError::DatabaseError)?;
987
988 let acquired: bool = row.get(0);
989
990 if acquired {
991 Ok(CoordinationGuard {
992 lock_key,
993 connection: Some(connection),
994 })
995 } else {
996 Err(CoordinationError::LeadershipNotAcquired {
998 subscription_name: subscription_name.to_string(),
999 })
1000 }
1001}
1002
1003impl ProjectorCoordinator for PostgresProjectorCoordinator {
1004 type Error = CoordinationError;
1005 type Guard = CoordinationGuard;
1006
1007 async fn try_acquire(&self, subscription_name: &str) -> Result<Self::Guard, Self::Error> {
1008 try_acquire_advisory_lock(&self.pool, subscription_name).await
1009 }
1010}
1011
1012impl ProjectorCoordinator for PostgresEventStore {
1013 type Error = CoordinationError;
1014 type Guard = CoordinationGuard;
1015
1016 async fn try_acquire(&self, subscription_name: &str) -> Result<Self::Guard, Self::Error> {
1017 try_acquire_advisory_lock(&self.pool, subscription_name).await
1018 }
1019}
1020
1021#[cfg(test)]
1022#[path = "lib.test.rs"]
1023mod tests;