Skip to main content

eventcore_postgres/projections/
error.rs

1use std::error::Error;
2
3use eventcore_types::{DeliveryPosition, DeliverySourceId, ProjectionSelectionId, ProjectorName};
4use thiserror::Error;
5
6use super::ProjectionConfigurationError;
7
8/// Type-erased failure source retained by the stable runner error boundary.
9pub type BoxedProjectionError = Box<dyn Error + Send + Sync>;
10
11/// Terminal failures from a transactional projection run.
12#[derive(Debug, Error)]
13#[non_exhaustive]
14pub enum TransactionalProjectionError {
15    /// Another process owns this projector's read-model leadership lock.
16    #[error("transactional projection leadership is busy")]
17    LeadershipBusy,
18    /// The leader session was lost and can no longer authorize writes.
19    #[error("transactional projection leadership was lost")]
20    LeadershipLost {
21        /// Underlying database error.
22        #[source]
23        source: BoxedProjectionError,
24    },
25    /// The event source could not provide a page or high-water mark.
26    #[error("transactional projection source failed")]
27    Source {
28        /// Underlying source error.
29        #[source]
30        source: BoxedProjectionError,
31    },
32    /// A selected persisted payload could not decode into the application's event contract.
33    #[error("transactional projection could not decode event at position {position:?}")]
34    Decode {
35        /// Position of the malformed selected envelope.
36        position: DeliveryPosition,
37        /// Underlying deserialization error.
38        #[source]
39        source: BoxedProjectionError,
40    },
41    /// The projector chose the `Fatal` decision for an application error.
42    #[error("transactional projection application failed at position {position:?}")]
43    Application {
44        /// Failed position.
45        position: DeliveryPosition,
46        /// Underlying application error.
47        #[source]
48        source: BoxedProjectionError,
49    },
50    /// Retry attempts were exhausted without committing the event effect.
51    #[error("transactional projection retries exhausted at position {position:?}")]
52    RetryExhausted {
53        /// Failed position.
54        position: DeliveryPosition,
55        /// Total application invocations, including the initial attempt.
56        attempts: u32,
57        /// Last application error.
58        #[source]
59        source: BoxedProjectionError,
60    },
61    /// Progress could not be loaded or advanced for the pending delivery position.
62    #[error("transactional projection progress operation failed at position {position:?}")]
63    Progress {
64        /// Delivery position whose transaction remains pending.
65        position: DeliveryPosition,
66        /// Underlying database error.
67        #[source]
68        source: BoxedProjectionError,
69    },
70    /// A progress storage operation without a pending delivery position failed.
71    #[error("transactional projection progress storage operation failed")]
72    ProgressStore {
73        /// Underlying database error.
74        #[source]
75        source: BoxedProjectionError,
76    },
77    /// PostgreSQL did not acknowledge the commit, so durable state must be inspected on recovery.
78    #[error("transactional projection commit acknowledgement is indeterminate")]
79    CommitIndeterminate {
80        /// Position whose transaction may or may not have committed.
81        position: DeliveryPosition,
82        /// Underlying database error.
83        #[source]
84        source: BoxedProjectionError,
85    },
86    /// Existing progress belongs to a different delivery source.
87    #[error("transactional projection source identity does not match {projector}")]
88    SourceIdentityMismatch {
89        /// Projector whose durable identity was incompatible.
90        projector: ProjectorName,
91        /// Source identity persisted with durable progress.
92        persisted: DeliverySourceId,
93        /// Source identity configured for this invocation.
94        configured: DeliverySourceId,
95    },
96    /// Existing progress belongs to a different projection selection.
97    #[error("transactional projection selection identity does not match {projector}")]
98    SelectionIdentityMismatch {
99        /// Projector whose durable identity was incompatible.
100        projector: ProjectorName,
101        /// Selection identity persisted with durable progress.
102        persisted: ProjectionSelectionId,
103        /// Selection identity configured for this invocation.
104        configured: ProjectionSelectionId,
105    },
106    /// Configuration cannot safely start a projection.
107    #[error("transactional projection configuration is invalid")]
108    Configuration {
109        /// Invalid configuration detail.
110        #[source]
111        source: ProjectionConfigurationError,
112    },
113    /// A confirmed commit succeeded but its after-commit action failed.
114    #[error(
115        "transactional projection after-commit action failed at position {committed_position:?}"
116    )]
117    AfterCommitFailed {
118        /// Already committed position.
119        committed_position: DeliveryPosition,
120        /// Underlying hook error.
121        #[source]
122        source: BoxedProjectionError,
123    },
124}
125
126/// Failures from a coordinated basic reset.
127#[derive(Debug, Error)]
128#[non_exhaustive]
129pub enum ProjectionResetError {
130    /// Another invocation owns this projector's leadership grant.
131    #[error("transactional projection reset leadership is busy")]
132    Busy,
133    /// The leader session was lost while starting, rolling back, or releasing the reset.
134    ///
135    /// A release failure can occur after the reset commit was acknowledged. Callers must not
136    /// infer from this variant that the reset was rolled back or did not commit.
137    #[error("transactional projection reset leadership was lost")]
138    LeadershipLost {
139        /// Underlying database error.
140        #[source]
141        source: BoxedProjectionError,
142    },
143    /// Existing progress belongs to a different delivery source.
144    #[error("transactional projection reset source identity does not match {projector}")]
145    SourceIdentityMismatch {
146        /// Projector whose durable identity was incompatible.
147        projector: ProjectorName,
148        /// Source identity persisted with durable progress.
149        persisted: DeliverySourceId,
150        /// Source identity configured for this reset.
151        configured: DeliverySourceId,
152    },
153    /// Existing progress belongs to a different projection selection.
154    #[error("transactional projection reset selection identity does not match {projector}")]
155    SelectionIdentityMismatch {
156        /// Projector whose durable identity was incompatible.
157        projector: ProjectorName,
158        /// Selection identity persisted with durable progress.
159        persisted: ProjectionSelectionId,
160        /// Selection identity configured for this reset.
161        configured: ProjectionSelectionId,
162    },
163    /// Application reset code failed and its transaction was rolled back.
164    #[error("transactional projection reset callback failed")]
165    Callback {
166        /// Underlying application error.
167        #[source]
168        source: BoxedProjectionError,
169    },
170    /// Progress validation or deletion failed before commit.
171    #[error("transactional projection reset progress operation failed")]
172    Progress {
173        /// Underlying database error.
174        #[source]
175        source: BoxedProjectionError,
176    },
177    /// PostgreSQL did not acknowledge the reset commit, so durable state is indeterminate.
178    #[error("transactional projection reset commit acknowledgement is indeterminate")]
179    CommitIndeterminate {
180        /// Underlying database error.
181        #[source]
182        source: BoxedProjectionError,
183    },
184}
185
186/// Failures from the reset-and-replay convenience operation.
187#[derive(Debug, Error)]
188#[non_exhaustive]
189pub enum ProjectionResetAndReplayError {
190    /// The coordinated reset phase failed.
191    #[error("transactional projection reset phase failed")]
192    Reset(#[source] ProjectionResetError),
193    /// The replay phase failed after reset committed.
194    #[error("transactional projection replay phase failed")]
195    Replay(#[source] TransactionalProjectionError),
196}