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}