@cratis/pi 2.20.1 → 2.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -116,7 +116,56 @@ public class CommandFilters(IInstancesOf<ICommandFilter> filters) : ICommandFilt
|
|
|
116
116
|
|
|
117
117
|
Every exception type in the codebase should communicate *what went wrong* in domain terms. Built-in types like `InvalidOperationException` tell you nothing about the problem — a custom `AuthorAlreadyRegistered` tells you everything.
|
|
118
118
|
|
|
119
|
-
-
|
|
119
|
+
- **Exceptions are for exceptional, unrecoverable state only — never for control flow.** "Unrecoverable" means the caller has no correct next step to take other than to stop: a violated invariant, a bug, a dependency that is simply gone. If a caller is expected to `catch` a specific type and then do something sensible and ordinary in response — retry, record an impediment, try a different branch, return a different response to a user — that outcome is not exceptional, it is a normal result of the operation, and it belongs in the method's return type, not in its throw list.
|
|
120
|
+
- **The tell: a caller-side `catch` block whose body is not "log and rethrow" or "crash louder".** A `catch (SomeException)` that goes on to do real application work — write a different event, set a different HTTP status, schedule a retry, record why something didn't happen — is exception-driven flow control wearing a `try`/`catch` costume. This applies even when the exception type is well-named and richly documented; a beautifully named exception thrown for an anticipated, recoverable outcome is still the wrong tool.
|
|
121
|
+
- **Wrong** — `IWorkerRuntime.Start` throwing `WorkerIsAlreadyRunning`/`WorkerIsStillGoingAway`/`WorkerLaunchWasRefused` for three entirely anticipated outcomes of trying to launch a worker, with the caller structured as one `try` and three `catch` blocks each doing real, different application work (recording a specific impediment, deciding whether to keep the callback token):
|
|
122
|
+
|
|
123
|
+
```csharp
|
|
124
|
+
try
|
|
125
|
+
{
|
|
126
|
+
await workerRuntime.Start(job, cancellationToken);
|
|
127
|
+
}
|
|
128
|
+
catch (WorkerIsStillGoingAway stillGoingAway)
|
|
129
|
+
{
|
|
130
|
+
await commandPipeline.Execute(new RecordDispatchImpediment(work.Id, "The previous worker is still shutting down"));
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
catch (WorkerIsAlreadyRunning alreadyRunning)
|
|
134
|
+
{
|
|
135
|
+
await commandPipeline.Execute(new RecordDispatchImpediment(work.Id, "A worker for this work is already running"));
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- **Right** — a `Cratis.Monads` return type naming every real outcome, so the compiler forces every caller to handle each one and nothing is discoverable only by reading a `<exception>` tag or by triggering it at runtime:
|
|
141
|
+
|
|
142
|
+
```csharp
|
|
143
|
+
public enum WorkerLaunchOutcome { Started, StillGoingAway, AlreadyRunning, RefusedByCluster }
|
|
144
|
+
|
|
145
|
+
Task<Result<WorkerLaunchOutcome, ClusterFailure>> Start(WorkerJob job, CancellationToken cancellationToken = default);
|
|
146
|
+
|
|
147
|
+
// caller:
|
|
148
|
+
var outcome = await workerRuntime.Start(job, cancellationToken);
|
|
149
|
+
if (!outcome.TryGetResult(out var launched))
|
|
150
|
+
{
|
|
151
|
+
// genuinely exceptional - the cluster itself failed in a way retry logic doesn't cover
|
|
152
|
+
throw new WorkerLaunchIrrecoverablyFailed(work.Id, outcome);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
var impediment = launched switch
|
|
156
|
+
{
|
|
157
|
+
WorkerLaunchOutcome.StillGoingAway => "The previous worker is still shutting down",
|
|
158
|
+
WorkerLaunchOutcome.AlreadyRunning => "A worker for this work is already running",
|
|
159
|
+
_ => null
|
|
160
|
+
};
|
|
161
|
+
if (impediment is not null)
|
|
162
|
+
{
|
|
163
|
+
await commandPipeline.Execute(new RecordDispatchImpediment(work.Id, impediment));
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
A single-outcome case uses `Result<TResult, TError>` directly rather than an enum; `Option<TValue>` is the equivalent for "a value, or nothing" with no error to describe. Reach for these from `Cratis.Monads` before reaching for a custom exception type whenever the "failure" is something a caller is meant to branch on rather than merely propagate.
|
|
120
169
|
- Always create a custom exception type that derives from `Exception`.
|
|
121
170
|
- Never use built-in exception types (`InvalidOperationException`, `ArgumentException`, etc.).
|
|
122
171
|
- Never suffix exception class names with `Exception` — `AuthorNotFound` reads better than `AuthorNotFoundException`.
|