@cratis/pi 7.61.0 → 7.62.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.
- package/package/corpus/agents/spec-writer.md +2 -2
- package/package/corpus/harnesses/copilot/instructions/application-profile.instructions.md +2 -2
- package/package/corpus/harnesses/copilot/instructions/glossary.instructions.md +2 -2
- package/package/corpus/harnesses/copilot/instructions/specs.scenarios.csharp.instructions.md +7 -4
- package/package/corpus/harnesses/copilot/instructions/vertical-slices.instructions.md +22 -13
- package/package/corpus/product-sync.json +3 -2
- package/package/corpus/prompts/add-business-rule.prompt.md +1 -1
- package/package/corpus/rules/application-profile.md +2 -2
- package/package/corpus/rules/glossary.md +2 -2
- package/package/corpus/rules/specs.scenarios.csharp.md +7 -4
- package/package/corpus/rules/vertical-slices.md +22 -13
- package/package/corpus/skills/cratis-application-slice-specifications/SKILL.md +4 -3
- package/package/corpus/skills/cratis-application-slice-specifications/references/query-and-decision-scenarios.md +12 -3
- package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/SKILL.md +12 -7
- package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md +11 -11
- package/package/corpus/skills/cratis-arc-command/SKILL.md +3 -2
- package/package/corpus/skills/cratis-arc-command/references/decisions-and-routing.md +95 -51
- package/package/corpus/skills/cratis-arc-command/references/fresh-identifiers.md +5 -2
- package/package/corpus/skills/cratis-arc-command/references/read-model-injection.md +9 -5
- package/package/corpus/skills/cratis-arc-command-validation/SKILL.md +99 -52
- package/package/corpus/skills/cratis-chronicle-decision-reads/SKILL.md +37 -15
- package/package/corpus/skills/cratis-chronicle-decision-reads/references/admission-and-ownership.md +3 -2
- package/package/corpus/skills/cratis-chronicle-decision-reads/references/choosing-how-a-rule-holds.md +277 -138
- package/package/corpus/skills/cratis-chronicle-event-constraints/SKILL.md +4 -4
- package/package/corpus/skills/cratis-chronicle-event-modeling/SKILL.md +6 -4
- package/package/corpus/skills/cratis-chronicle-read-model/SKILL.md +2 -1
- package/package/corpus/skills/cratis-chronicle-reducer/SKILL.md +6 -3
- package/package/corpus/skills/cratis-code-review/SKILL.md +6 -3
- package/package/corpus/skills/cratis-screenplay-streams-and-consistency/references/consistency-and-concurrency.md +5 -3
- package/package/corpus/skills/cratis-specifications-csharp/SKILL.md +3 -2
- package/package/corpus/skills/cratis-specifications-csharp/references/application-scenarios.md +1 -1
- package/package.json +1 -1
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Contents
|
|
4
4
|
|
|
5
|
-
-
|
|
5
|
+
- Defaults
|
|
6
|
+
- Route by rule shape
|
|
6
7
|
- The matrix
|
|
7
|
-
-
|
|
8
|
+
- Reads under Off, Measure and On
|
|
8
9
|
- Constraints
|
|
9
|
-
-
|
|
10
|
+
- What a read guard covers
|
|
11
|
+
- Refusals
|
|
10
12
|
- Arc traps
|
|
11
13
|
- Chronicle-only traps
|
|
12
|
-
-
|
|
14
|
+
- Roll out with Measure
|
|
15
|
+
- Test the race
|
|
16
|
+
- Costs
|
|
13
17
|
- Checklist for generated code
|
|
14
18
|
|
|
15
19
|
<!-- Copyright (c) Cratis. All rights reserved. -->
|
|
@@ -20,73 +24,133 @@ question: **which mechanism makes this rule hold while other writers append at t
|
|
|
20
24
|
same time**, and which look correct but do not. Read it before writing a command,
|
|
21
25
|
validator, endpoint or handler that reads state and then appends.
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
this agrees with:
|
|
27
|
+
Published guides this agrees with:
|
|
25
28
|
[Chronicle: choosing how a rule holds](https://www.cratis.io/chronicle/dynamic-consistency-boundary/choosing/),
|
|
26
29
|
[decision reads](https://www.cratis.io/chronicle/read-models/decision-reads/),
|
|
27
30
|
[Arc: choosing how a command's rule holds](https://www.cratis.io/arc/backend/csharp/chronicle/consistency/).
|
|
28
31
|
|
|
29
|
-
##
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
32
|
+
## Defaults
|
|
33
|
+
|
|
34
|
+
- Read protection is `Off` unless the project sets it. New code starts with
|
|
35
|
+
`On`: `<CratisReadProtection>On</CratisReadProtection>` in the project (it
|
|
36
|
+
emits `[assembly: ReadProtection(ReadProtectionMode.On)]`; an invalid value
|
|
37
|
+
fails the build). Existing code moves `Off` to `Measure` to `On`; see
|
|
38
|
+
[Roll out with Measure](#roll-out-with-measure).
|
|
39
|
+
- Under `On`, a read model that is a parameter of `Handle()` or of a
|
|
40
|
+
`CommandValidator<T>` constructor is read fresh from the event log and guards
|
|
41
|
+
the command's append. The state rule goes in the validator with the read model
|
|
42
|
+
as a constructor parameter; `Handle()` returns the events.
|
|
43
|
+
- `[ProtectedDecision]` with `DecisionRead<T>` is the older per-command opt-in. It
|
|
44
|
+
still works and is not obsolete. Use it only in a project that is not on `On`.
|
|
45
|
+
It is not redundant with `On`; do not remove it because `On` is set without
|
|
46
|
+
checking what the command takes.
|
|
47
|
+
- Subject ids derive from `EventSourceId` (`record Isbn(string Value) :
|
|
48
|
+
EventSourceId(Value)`), so the command needs no `[Key]`.
|
|
49
|
+
|
|
50
|
+
## Route by rule shape
|
|
51
|
+
|
|
52
|
+
Take the first step that matches. Names S1 to S14 label the shapes used in the
|
|
53
|
+
published scenario catalogue.
|
|
54
|
+
|
|
55
|
+
1. **A value is unique, or something exists once** (S2: email, ISBN, one card per
|
|
56
|
+
member): a constraint. Not a read, not a validator.
|
|
57
|
+
2. **The rule uses data outside Chronicle events** (S11): record the external fact
|
|
58
|
+
as an event first, then decide on it; otherwise accept the risk and repair.
|
|
59
|
+
3. **A rare violation is acceptable, a hint, or a hot key** (S11): no guard.
|
|
60
|
+
Append and repair with a reactor that returns a correcting event, or mark a
|
|
61
|
+
deliberate advisory read `[Unprotected("why")]`.
|
|
62
|
+
4. **Someone edited what someone else changed** (S10: the user saw revision N): an
|
|
63
|
+
explicit concurrency scope built from the revision the client sent.
|
|
64
|
+
5. **One entity's state** (S1: last copy, balance, count): the read model as a
|
|
65
|
+
parameter under `On`, the rule in a validator. Not an aggregate for one counter.
|
|
66
|
+
6. **Two entities** (S3 loan limit and last copy; S9 course capacity and student
|
|
67
|
+
limit): two read-model parameters, the second chosen by convention or
|
|
68
|
+
`[LookUpBy(nameof(Command.Property))]`, and `Handle()` returns
|
|
69
|
+
`IEnumerable<EventForEventSourceId>` with one event per entity in one atomic
|
|
70
|
+
append. Use distinct event types per entity.
|
|
71
|
+
7. **A limit inside one stream of one source** (S4: 5000 per account per month): a
|
|
72
|
+
per-stream read model, `[ReadModel, EventStream<TSource>("Transactions")]`, with
|
|
73
|
+
the command routed `[EventSource<TSource>("Transactions")]` and
|
|
74
|
+
`[EventStreamId("{Month}")]`. For a short stream and a question about raw events
|
|
75
|
+
(S5: approve a period once), a stream decision with `IStreamReads`.
|
|
76
|
+
8. **Process stream with steps and completion** (S6): a definition with a stream per
|
|
77
|
+
process, declared concurrency and a closed stream.
|
|
78
|
+
9. **Gapless numbers** (S7): a numbering source read with `IDecisionReads` in
|
|
79
|
+
`Provide()`; `[Unique]` alone gives no duplicates but allows gaps.
|
|
80
|
+
10. **Rich behaviour over one lifecycle** (S8: order lines, pay, ship, cancel): an
|
|
81
|
+
aggregate root, returning `Commit()` last. An aggregate and a protected read
|
|
82
|
+
of the same source in one command are refused.
|
|
83
|
+
11. **Moving an existing app to `On`** (S12): `Measure` first.
|
|
84
|
+
12. **A command failed under protection** (S13): the [refusals](#refusals).
|
|
85
|
+
13. **Prove it** (S14): [test the race](#test-the-race).
|
|
53
86
|
|
|
54
87
|
## The matrix
|
|
55
88
|
|
|
56
89
|
| Rule shape | Use | Never use | Too much |
|
|
57
90
|
| --- | --- | --- | --- |
|
|
58
|
-
| Unique value | `[Unique]` property or `IConstraint` | read
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
| Rare violation
|
|
66
|
-
|
|
|
67
|
-
| Hot key, long history | shorten histories (split by period or stream); constraint for uniqueness | ignoring cost: each
|
|
68
|
-
|
|
69
|
-
Not a guard: a plain read
|
|
70
|
-
`ValidationResult.Error`, `concurrency: true`, the default optimistic
|
|
71
|
-
reactor, or an `ExpectedRevision`
|
|
72
|
-
|
|
73
|
-
##
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
| Backing | Freshness | Guard |
|
|
91
|
+
| S2 Unique value, create once | `[Unique]` property or event type, `IConstraint` | "exists?" read then append | read model or decision read (they guard one key's history) |
|
|
92
|
+
| S1 One entity's state | read-model parameter + validator under `On` | validator over a plain read under `Off` (a hint) | aggregate for one counter |
|
|
93
|
+
| S3, S9 Two entities | two parameters, `[LookUpBy]` for the second key, `EventForEventSourceId` per key | a guard on one side only, two separate appends, one read model keyed through an event property | none |
|
|
94
|
+
| S4 Limit inside one stream | per-stream read model on a routed command | whole-source model when another stream's events must not conflict | none |
|
|
95
|
+
| S5 Once per period in a short stream | stream decision (`IStreamReads`) or per-stream model | whole-source read | none |
|
|
96
|
+
| S8 Rich lifecycle | aggregate root | aggregate plus a read of the same source | aggregate for one rule |
|
|
97
|
+
| S10 Edit conflict | explicit scope from the client's revision | `concurrency: true` alone (reads the tail after `Handle`) | constraint |
|
|
98
|
+
| S11 Rare violation, hint, external data | repair reactor, `[Unprotected("why")]`, or record the external fact first | presenting a plain read as a guarantee | any guard |
|
|
99
|
+
| S12 Existing app | `Measure`, then `On` per project | switching a whole app to `On` unmeasured | none |
|
|
100
|
+
| Hot key, long history | shorten histories (split by period or stream); constraint for uniqueness | ignoring cost: each guarded read folds the key's history | none |
|
|
101
|
+
|
|
102
|
+
Not a guard: a plain read under `Off` or `Measure`, a validator over such a read,
|
|
103
|
+
`Result<,>`, `ValidationResult.Error`, `concurrency: true`, the default optimistic
|
|
104
|
+
append, a reactor, or an `ExpectedRevision` never passed into a scope.
|
|
105
|
+
|
|
106
|
+
## Reads under Off, Measure and On
|
|
107
|
+
|
|
108
|
+
| Mode | Read-model parameter, validator constructor | Results and status codes |
|
|
78
109
|
| --- | --- | --- |
|
|
79
|
-
|
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
110
|
+
| `Off` (default) | plain read; a hint, guards nothing | as today |
|
|
111
|
+
| `Measure` | plain read; Arc reports what `On` would do | same as `Off` |
|
|
112
|
+
| `On` | read fresh from the log; guards the command's append | a lost race is HTTP 400 |
|
|
113
|
+
|
|
114
|
+
A command overrides the project with `[ReadProtection(ReadProtectionMode.On)]`
|
|
115
|
+
(`Cratis.Arc.Commands`); command-level `[Unprotected]` means `Off`. The runtime
|
|
116
|
+
cap `ReadProtection:MaximumMode` only lowers (a missing or invalid value is `On`,
|
|
117
|
+
no cap); a lowered command logs a startup Warning. With the cap set, the `On`
|
|
118
|
+
behaviour does not hold.
|
|
119
|
+
|
|
120
|
+
A plain read does not capture a position even when the model is `[Passive]`:
|
|
121
|
+
passive is fresher, not safer. Do not rebuild a guard by hand (capturing the tail
|
|
122
|
+
yourself before a read): the read may be older than the position you captured.
|
|
123
|
+
|
|
124
|
+
Under `On`:
|
|
125
|
+
|
|
126
|
+
- `IReadModels`, `IMongoCollection` and detached reads in the command are refused.
|
|
127
|
+
Take the model as a parameter, or mark the method `[Unprotected("why")]` (it
|
|
128
|
+
covers the body, not injected parameters).
|
|
129
|
+
- Immediate appends (an injected `IEventLog.Append`) are refused. Return the events.
|
|
130
|
+
- The key is, in order: `[LookUpBy(nameof(Cmd.Prop))]` on the parameter
|
|
131
|
+
(parameter-only attribute, `Cratis.Arc.Commands`); the single command property
|
|
132
|
+
whose type equals the model's `Id` type; `AmbiguousReadModelLookup` (HTTP 500) if
|
|
133
|
+
several match; the command's event source id.
|
|
134
|
+
- Admitted: event-source-keyed model-bound or declarative projections with a
|
|
135
|
+
string or Guid `Id`, and unfiltered reducers without constructor dependencies
|
|
136
|
+
(except `ILogger<T>`; self-reducing models are exempt). Refused with
|
|
137
|
+
`ReadCannotBeProtected` unless `[Unprotected("why")]`: joins and children, a
|
|
138
|
+
model keyed by an event property, Pinned generation delivery, filtered reducers,
|
|
139
|
+
reducers with dependencies, other event sequences.
|
|
140
|
+
- A validator's read models are protected when the validator is convention
|
|
141
|
+
registered (transient). Hand-registered scoped, singleton, factory or instance
|
|
142
|
+
validators are refused.
|
|
143
|
+
- Under `[ProtectedDecision]` without `On`, only a parameterless validator runs.
|
|
144
|
+
|
|
145
|
+
Caller severity: model-bound commands accept `X-Allowed-Severity` (`allowedSeverity`).
|
|
146
|
+
Under `Off` and `Measure` a caller sending `Error` removes Error results from validators,
|
|
147
|
+
command filters and `Provide()`, so `Handle()` runs, unless the command declares
|
|
148
|
+
`[BlockOnValidationSeverity(ValidationResultSeverity.Error)]`. Requires Arc 22.108.0:
|
|
149
|
+
a command declared `On` gets an `Error` floor (`Error` and `Unknown` always block; the
|
|
150
|
+
caller can relax only `Warning` and `Information`). Lowering
|
|
151
|
+
`ReadProtection:MaximumMode` at runtime does not remove it, and an explicit attribute
|
|
152
|
+
wins. Before that version a validator's state rule can be skipped by the caller even
|
|
153
|
+
under `On`.
|
|
90
154
|
|
|
91
155
|
## Constraints
|
|
92
156
|
|
|
@@ -94,91 +158,113 @@ A constraint binds every writer, whichever client or service appends, per namesp
|
|
|
94
158
|
and per event sequence. It cannot count, sum or test a predicate; it expresses
|
|
95
159
|
uniqueness (with release on a removal event), create-once and closed streams.
|
|
96
160
|
|
|
97
|
-
- The claim is settled after the durable append (not yet
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
unconditional proof.
|
|
161
|
+
- The claim is settled after the durable append (not yet implemented in every
|
|
162
|
+
store). A failed index update is logged and a later duplicate may pass until a
|
|
163
|
+
reindex. SQL and InMemory stores do not yet settle claims. A constraint is the
|
|
164
|
+
right mechanism, not an unconditional proof.
|
|
102
165
|
- Tenants in different namespaces can claim the same value.
|
|
103
|
-
- `[RemoveConstraint]` releases attribute constraints (`[Unique]` on a property
|
|
104
|
-
|
|
105
|
-
|
|
166
|
+
- `[RemoveConstraint]` releases attribute constraints (`[Unique]` on a property or
|
|
167
|
+
on the event type); on a fluent `IConstraint` it compiles and does nothing, so
|
|
168
|
+
use `.RemovedWith<T>()` there.
|
|
106
169
|
- It is not re-run when the kernel retries a duplicate sequence number.
|
|
107
170
|
- For a friendly message use `.WithMessage`; Arc maps a violation to HTTP 400.
|
|
108
171
|
|
|
109
|
-
##
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
- **
|
|
118
|
-
|
|
119
|
-
self-reducing models, joins, children, `FromEvery`, event-property routing and
|
|
120
|
-
other sequences are refused. A model without `Id`, or keyed by a concept that does
|
|
121
|
-
not convert, refuses with `KeyConversion`; in Arc that surfaces as HTTP 500.
|
|
122
|
-
- **Absent models are guarded.** A concurrent creation conflicts. Do not fabricate a
|
|
123
|
-
default instance for an absent model.
|
|
172
|
+
## What a read guard covers
|
|
173
|
+
|
|
174
|
+
Under `On` (and for a Chronicle decision read), if an event of the model's types
|
|
175
|
+
for that key arrives after the read, nothing from the command is written, whoever
|
|
176
|
+
wrote the event. The kernel checks the stored tail filtered by key, types and route.
|
|
177
|
+
|
|
178
|
+
- **Only that key and those types.** A concurrent event of another type (a rename)
|
|
179
|
+
does not conflict.
|
|
180
|
+
- **Absent models are guarded.** A concurrent creation conflicts. Do not fabricate
|
|
181
|
+
a default instance for an absent model.
|
|
124
182
|
- **Cross-key.** A read on key X guards X even when the command appends only to Y.
|
|
125
|
-
Per-key reads cover only keys in the read set
|
|
126
|
-
("a member holds at most three loans" with each loan on its own source), write
|
|
127
|
-
facts on a key that is read, usually the member's.
|
|
128
|
-
- **Every deciding writer must be guarded.** The guard
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
183
|
+
Per-key reads cover only keys in the read set; when new keys can join the set
|
|
184
|
+
("a member holds at most three loans" with each loan on its own source), write
|
|
185
|
+
the facts on a key that is read, usually the member's.
|
|
186
|
+
- **Every deciding writer must be guarded.** The guard protects only the decision
|
|
187
|
+
that carries it. A writer that decides on the same facts through a plain read, an
|
|
188
|
+
`[Unprotected]` read or a direct append can still append on stale state. The rule
|
|
189
|
+
holds only when every writer that decides on it is guarded, or when it is a
|
|
190
|
+
constraint.
|
|
191
|
+
- **Outside the guard:** revisions and redactions (append conditions do not detect
|
|
192
|
+
them), services and external stores, other event sequences, `[Unprotected]`
|
|
193
|
+
reads, `Off` mode, projection definition changes, and writes that bypass the
|
|
194
|
+
kernel's serialized append path (duplicate grain activations, direct storage
|
|
195
|
+
writes).
|
|
196
|
+
- **Conflict.** Arc and Chronicle do not re-run the command; the fold of a read
|
|
197
|
+
may retry up to three times. The client reads fresh state and decides again.
|
|
198
|
+
- **Aggregates.** Under `On`, an aggregate and a protected read of the same source
|
|
199
|
+
are refused (`ReadAndAggregateOnSameSource`); a read of a different source is
|
|
200
|
+
fine. Arc guards an aggregate's commit with a scope from its rehydration position.
|
|
201
|
+
- **Empty protected commit.** A unit that enrolled a read and staged no events
|
|
202
|
+
appends an empty batch with the guard scopes. Current kernels validate it and
|
|
203
|
+
write nothing; a kernel that rejects an empty batch surfaces
|
|
204
|
+
`DecisionReadValidateOnlyNotSupported`, which fails closed. Assert it in an
|
|
205
|
+
integration specification before relying on it.
|
|
206
|
+
- **Chronicle model.** Chronicle supports decision models and append conditions
|
|
207
|
+
within its event-source model: event source id, event source type, event stream
|
|
208
|
+
type and stream id play the role of tags, one value each per event. It is not a
|
|
209
|
+
DCB event store: an event belongs to one event source, and a fact about two
|
|
210
|
+
entities is two events in one atomic append. Custom tags take no part in
|
|
211
|
+
consistency.
|
|
212
|
+
|
|
213
|
+
## Refusals
|
|
214
|
+
|
|
215
|
+
| Seen by the caller | Cause | Do |
|
|
216
|
+
| --- | --- | --- |
|
|
217
|
+
| HTTP 400, `ValidationResult` reason `concurrencyViolation`, `reasonDetail` `ReadChanged`, `state` `{ readModel, key }` | an event of the read model's types for that key arrived after the read; nothing was written | resubmit; the client reads fresh state and decides again |
|
|
218
|
+
| HTTP 400, `ReadIncomplete` | the fold did not finish after three attempts | resubmit |
|
|
219
|
+
| HTTP 500 | the command cannot be guarded: a code or configuration error | change the code; retrying does not help |
|
|
220
|
+
|
|
221
|
+
The `ReadChanged` message reads "BookStock 'isbn-1' changed after BorrowBook read
|
|
222
|
+
it. Nothing from this command was written; submit it again." It does not name the
|
|
223
|
+
conflicting event. Outside Development, a 500 message is replaced unless
|
|
224
|
+
`ArcOptions.ExposeExceptionDetails` is set. Frontend (TypeScript):
|
|
225
|
+
`CommandResult.isSuccess/isValid/validationResults`, `ValidationResult.reason`
|
|
226
|
+
(`'concurrencyViolation'`, `ValidationResultReason` from `@cratis/arc/validation`),
|
|
227
|
+
`reasonDetail` (a string with no TS constant), `state`.
|
|
228
|
+
|
|
229
|
+
Common 500 refusals: `ReadCannotBeProtected` (shape, above),
|
|
230
|
+
`AmbiguousReadModelLookup` (add `[LookUpBy]`), `ImmediateAppendWhileReadsAreProtected`
|
|
231
|
+
(return the event), `ReadRefusedWhileReadsAreProtected` and
|
|
232
|
+
`DetachedReadWhileReadsAreProtected` (parameter, `IDecisionReads.Get` in
|
|
233
|
+
`Provide()`, or `[Unprotected("why")]`), `ReadAfterProtectedCommit` (read first,
|
|
234
|
+
return `Commit()` last), `ReadOfSourceWithStagedEvents`, `ExplicitScopeDoesNotCoverReads`,
|
|
235
|
+
`DeclaredRoutingNotCoveredByExplicitScope`, `ConflictingReadProtection`,
|
|
236
|
+
`ReadModelStreamNotOnCommandRoute`.
|
|
156
237
|
|
|
157
238
|
## Arc traps
|
|
158
239
|
|
|
159
240
|
| Trap | What happens | Fix |
|
|
160
241
|
| --- | --- | --- |
|
|
161
|
-
| Read model
|
|
162
|
-
|
|
|
163
|
-
|
|
|
164
|
-
| `concurrency: true` on `EventStreamId`, `EventStreamType` or `EventSourceType` | reads the narrowed tail after `Handle`; changes granularity, not the decision. A command that also reads `IReadModels` still triggers `ARCCHR0011` | decision read, explicit scope or stream decision |
|
|
242
|
+
| Read model parameter or validator under `Off` or `Measure`, then events returned | plain read; two requests both win | set `On`, or use a constraint or stream decision |
|
|
243
|
+
| Validator over a plain read under `Off` | a hint with a friendly message; guards nothing | `On` |
|
|
244
|
+
| `concurrency: true` on `EventStreamId`, `EventStreamType` or `EventSourceType` | reads the narrowed tail after `Handle`; changes granularity, not the decision | `On`, explicit scope or stream decision |
|
|
165
245
|
| Default (no attributes) | Arc reads the tail when the unit of work commits, after `Handle` | only protects the window between that read and the write |
|
|
166
|
-
| First append into a scope | unchecked unless `ChronicleOptions.ConcurrencyOptions.CheckFirstAppendIntoAScope` is set | constraint, or an aggregate
|
|
167
|
-
|
|
|
246
|
+
| First append into a scope | unchecked unless `ChronicleOptions.ConcurrencyOptions.CheckFirstAppendIntoAScope` is set | constraint, or an aggregate |
|
|
247
|
+
| A `UsingKey(e => e.Member)` projection read | refused (`ReadCannotBeProtected`) | append a fact to the entity the model is keyed by |
|
|
248
|
+
| Two or more key candidates (`EventSourceId`-typed, `[Key]`-marked or implicitly convertible) | `ARCCHR0002` warns, and `[Key]` does not silence it; Arc takes the first eligible property in declaration order | implement `ICanProvideEventSourceId` |
|
|
249
|
+
| Per-stream model on a command without a route | `ReadModelStreamNotOnCommandRoute` (500, analyzer `ARCCHR0037`) | add `[EventSource<TSource>("Stream")]` and `[EventStreamId("{Prop}")]` |
|
|
250
|
+
| Whole-source read beside a per-stream read of the same source | both drop the stream dimensions (Measure reports `coarsened`) | keep one granularity |
|
|
168
251
|
| No id property | a random event source id per call; nothing can conflict | give the command a key |
|
|
169
|
-
| `[Unprotected]` command taking `DecisionRead<T>` | resolves to an unguarded read | use `[ProtectedDecision]` |
|
|
170
252
|
| Injected `IEventLog.Append` in `Handle` | immediate write that survives a conflict rollback | return events |
|
|
171
253
|
| Aggregate `Commit()` whose result is ignored, or `Failed()` without `Commit()` | staged events still commit | return `await aggregate.Commit()` |
|
|
172
|
-
|
|
|
173
|
-
|
|
|
174
|
-
| Tests without `UseDecisionReads()` and a competing `AppendConcurrently` | the race is never exercised | `CommandScenario` with decision reads |
|
|
254
|
+
| `[Unprotected]` command taking `DecisionRead<T>` | resolves to an unguarded read | remove `[Unprotected]` |
|
|
255
|
+
| Tests without `AppendConcurrently`, or a scenario left in legacy mode | the race is never exercised | [test the race](#test-the-race) |
|
|
175
256
|
|
|
176
257
|
## Chronicle-only traps
|
|
177
258
|
|
|
178
259
|
- Decision read, then plain `IEventLog.Append`: the immediate append bypasses the
|
|
179
260
|
guard. Stage with `Transactional.Append`, or pass `guardedBy:` to `AppendMany`.
|
|
180
261
|
- `Get<T>` needs an ambient unit of work. `GetDetached<T>` enrolls nothing until you
|
|
181
|
-
pass it to `AppendMany(..., guardedBy:)` or `AddDecisionRead`.
|
|
262
|
+
pass it to `AppendMany(..., guardedBy:)` or `AddDecisionRead`. A unit can also
|
|
263
|
+
call `EnforceReadProtection()` and `AllowReads(ReadAllowance.Advisory|Contributed,
|
|
264
|
+
reason)`; `GetAppendConditions()` returns the `AppendCondition` values
|
|
265
|
+
(`Cratis.Chronicle.Transactions`).
|
|
266
|
+
- Stream reads: `IReadModels.GetInstanceById<T>(EventSourceId, EventStreamId)` throws
|
|
267
|
+
`StreamScopeNotSupported` unless the implementation supports it (analyzer CHR0064).
|
|
182
268
|
- The ASP.NET Core middleware commits **after** the response. Call
|
|
183
269
|
`IUnitOfWorkCompletionFeature.CommitAsync()` before responding and check
|
|
184
270
|
`IsSuccess` and `GetDecisionConflicts()`; the middleware reports no concurrency
|
|
@@ -191,22 +277,75 @@ stored tail filtered by key, types and route). See the skill's
|
|
|
191
277
|
- Arc has a static class also named `ConcurrencyScopeBuilder`; in an Arc project
|
|
192
278
|
qualify Chronicle's builder.
|
|
193
279
|
|
|
194
|
-
##
|
|
280
|
+
## Roll out with Measure
|
|
281
|
+
|
|
282
|
+
Steps: update the packages, set `<CratisReadProtection>Measure</CratisReadProtection>`,
|
|
283
|
+
run the application and its tests, read the report, fix or annotate each finding,
|
|
284
|
+
then set `On` per project. Behaviour for callers does not change under `Measure`.
|
|
285
|
+
|
|
286
|
+
- The report: meter `Cratis.Arc.ReadProtection`, counter
|
|
287
|
+
`cratis.arc.read_protection.events` (tags `command`, `category`, `model`,
|
|
288
|
+
`reason`), span event `read-protection.plan`, and a Warning once per command and
|
|
289
|
+
category for `refused_read`, `immediate_append` and `scope_refusal`.
|
|
290
|
+
- Categories: `protected_read`, `refused_read` (reason names the cause),
|
|
291
|
+
`advisory_read`, `unknown_dependency`, `redirect` (add `[LookUpBy]`),
|
|
292
|
+
`immediate_append` (return the event), `conflict_today`, `self_write`,
|
|
293
|
+
`coarsened`, `displacement`, `scope_refusal`, `matching_events_since_start`
|
|
294
|
+
(someone else's matching event arrived mid-command: `On` might have returned
|
|
295
|
+
400, a real race), `measurement_unavailable`, `mode_lowered`.
|
|
296
|
+
- Measure is best effort: one extra awaited call per command plus sampled
|
|
297
|
+
background scans. A report is a set of hypotheses.
|
|
298
|
+
- Fixes: reducer with dependencies or filters, join or children, event-property
|
|
299
|
+
key, Pinned delivery or `IMongoCollection`: change the read as the refusal says,
|
|
300
|
+
or annotate `[Unprotected("why")]`. Under `On`, `ARCCHR0011` no longer applies;
|
|
301
|
+
fix `ARCCHR0024`, `0028` and `0033`.
|
|
302
|
+
|
|
303
|
+
## Test the race
|
|
304
|
+
|
|
305
|
+
`new CommandScenario<T>()` selects the in-process decision log from the command's
|
|
306
|
+
read-protection mode and dependencies (for example under `On`); do not call
|
|
307
|
+
`UseDecisionReads()` (it is hidden and unnecessary). An `Off` command with no
|
|
308
|
+
protected dependency stays in legacy mode with pinned read models, which cannot
|
|
309
|
+
prove a race. Seed with
|
|
310
|
+
`scenario.Given.ForEventSource(id).Events(...)` (for a routed stream
|
|
311
|
+
`.OnRoute(route).Events(...)`). Guarded state needs events: protected reads fold the
|
|
312
|
+
event log and ignore a pinned model, and `DecisionRead<T>` parameters reject one.
|
|
313
|
+
`.ReadModel(m)` only pins an advisory (plain) read. Then queue a competing event with
|
|
314
|
+
`scenario.AppendConcurrently(id, events...)` and execute:
|
|
315
|
+
|
|
316
|
+
```csharp
|
|
317
|
+
await using var scenario = new CommandScenario<BorrowBook>();
|
|
318
|
+
scenario.Given.ForEventSource(isbn).Events(new BookAdded(Copies: 1));
|
|
319
|
+
scenario.AppendConcurrently(isbn, new BookBorrowed(other));
|
|
320
|
+
var result = await scenario.Execute(new BorrowBook(isbn, me));
|
|
321
|
+
result.ShouldHaveValidationErrorBecauseOf(ValidationResultReason.ConcurrencyViolation);
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
There is no `scenario.AppendConditions`. A pure decision test builds the command
|
|
325
|
+
and the validator with a hand-made read model. Chronicle-direct code asserts
|
|
326
|
+
`unitOfWork.GetAppendConditions()`. A default `EventScenario` scope strategy
|
|
327
|
+
returning `None` is not concurrency evidence.
|
|
328
|
+
|
|
329
|
+
## Costs
|
|
195
330
|
|
|
196
|
-
A
|
|
197
|
-
key's whole history of the model's types on every read, shares a per-model-type
|
|
331
|
+
A guarded read makes about four calls per attempt (up to three attempts), folds
|
|
332
|
+
the key's whole history of the model's types on every read, shares a per-model-type
|
|
198
333
|
projection grain across keys and tenants, and is not retried on conflict. Use it
|
|
199
334
|
where histories are short and write contention per key is low; shorten histories
|
|
200
335
|
before reaching for it on a hot key. A constraint costs one index lookup inside the
|
|
201
|
-
append. Decision reads
|
|
336
|
+
append. Decision reads exist in the .NET and Elixir clients.
|
|
202
337
|
|
|
203
338
|
## Checklist for generated code
|
|
204
339
|
|
|
205
|
-
- Name the mechanism and say why the rule shape needs it. A plain read,
|
|
206
|
-
`Result<,>` or `concurrency: true` is never presented as
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
-
|
|
212
|
-
-
|
|
340
|
+
- Name the mechanism and say why the rule shape needs it. A plain read, a
|
|
341
|
+
validator under `Off`, `Result<,>` or `concurrency: true` is never presented as
|
|
342
|
+
the guard.
|
|
343
|
+
- New code sets `On`. Uniqueness is a constraint. A state rule is a read-model
|
|
344
|
+
parameter in a validator, a per-stream model, a stream decision, an aggregate or
|
|
345
|
+
an explicit scope, enrolled **before** the events are staged.
|
|
346
|
+
- The model is admitted (source-keyed projection or admitted reducer, `Id`).
|
|
347
|
+
- Cross-key rules append one event per source atomically, with distinct event types.
|
|
348
|
+
- A specification interleaves a competing append and asserts
|
|
349
|
+
`ConcurrencyViolation`.
|
|
350
|
+
- What stays unguarded (an unguarded writer's own decision, external state,
|
|
351
|
+
revisions, `[Unprotected]` reads, `Off` mode) is stated.
|
|
@@ -15,7 +15,7 @@ Constraints are discovered automatically. There is no registration call.
|
|
|
15
15
|
|
|
16
16
|
A constraint binds **every** writer, whichever client or service appends, so it is
|
|
17
17
|
the mechanism for uniqueness and create-once. It cannot count, sum or test a
|
|
18
|
-
predicate: capacity and stock rules need a
|
|
18
|
+
predicate: capacity and stock rules need a guarded read (read protection `On`), a stream decision or an
|
|
19
19
|
aggregate. Read [choosing how a rule holds](../cratis-chronicle-decision-reads/references/choosing-how-a-rule-holds.md)
|
|
20
20
|
when it is unclear which one a rule needs. Limits: the claim is settled after the
|
|
21
21
|
durable append, so a failed index update can let a later duplicate
|
|
@@ -43,9 +43,9 @@ memory.
|
|
|
43
43
|
- The rule rejects **command input** — a range, a format, a cross-field
|
|
44
44
|
condition: use `cratis-arc-command-validation`.
|
|
45
45
|
- The rule is state-dependent (a count, a capacity, a status) and needs
|
|
46
|
-
concurrency protection: use
|
|
47
|
-
`cratis-arc-command-validation` and `cratis-chronicle-decision-reads`.
|
|
48
|
-
|
|
46
|
+
concurrency protection: use read protection `On` or stream decision reads with
|
|
47
|
+
`cratis-arc-command-validation` and `cratis-chronicle-decision-reads`. Injected
|
|
48
|
+
models and validators under `Off` (the default), and `Result<,>` alone, remain advisory.
|
|
49
49
|
- You are creating or changing a read model that the rule reads: use
|
|
50
50
|
`cratis-chronicle-read-model`.
|
|
51
51
|
- You are writing the specification and want the `EventScenario` mechanics in
|
|
@@ -156,14 +156,16 @@ If a field can only be filled by reaching into another behavior's read model to
|
|
|
156
156
|
a wrong stream boundary. Fix the model. A read-only cross-slice read through
|
|
157
157
|
Chronicle is allowed where the rule needs it (an injected read model read by
|
|
158
158
|
explicit key, or a `[Passive]` projection for a current read; a rule that must hold under
|
|
159
|
-
concurrency needs a decision
|
|
159
|
+
concurrency needs read protection `On`, a stream decision or a constraint, see
|
|
160
160
|
[choosing how a rule holds](../cratis-chronicle-decision-reads/references/choosing-how-a-rule-holds.md)); see
|
|
161
161
|
Cross-slice patterns in `rules/vertical-slices.md`. Make such a read deliberate
|
|
162
162
|
and name it in the brief, with the consistency it needs: a rule that must hold
|
|
163
163
|
under concurrency needs enrolled guarded reads, not plain injection or a stale
|
|
164
|
-
materialized snapshot. In Arc
|
|
165
|
-
|
|
166
|
-
`
|
|
164
|
+
materialized snapshot. In Arc use read protection `On` with the read model as a
|
|
165
|
+
validator or `Handle()` parameter for projected state, a per-stream read model or
|
|
166
|
+
`IStreamReads`/`StreamDecision` for one stream (`[ProtectedDecision]` +
|
|
167
|
+
`DecisionRead<T>` is the older per-command opt-in); `Result<,>` alone supplies
|
|
168
|
+
rejection, not a concurrency guard.
|
|
167
169
|
|
|
168
170
|
## Compliance modeling
|
|
169
171
|
|
|
@@ -129,7 +129,8 @@ builder when switching and verify storage/replay separately from output equality
|
|
|
129
129
|
Read [projection behavior](../cratis-chronicle-projection/SKILL.md) for mappings
|
|
130
130
|
and [reducers](../cratis-chronicle-reducer/SKILL.md) for complete signatures,
|
|
131
131
|
null deletion, replay hooks and preserving admission/ordering guards. Reducers
|
|
132
|
-
are source-keyed
|
|
132
|
+
are source-keyed and have no native joins; decision reads and Arc read protection `On`
|
|
133
|
+
admit only unfiltered reducers without constructor dependencies.
|
|
133
134
|
|
|
134
135
|
## Step 4 — Add a query surface, if the model is read from outside
|
|
135
136
|
|
|
@@ -214,9 +214,12 @@ model type** and makes it passive as a whole: no sink, nothing materialized, and
|
|
|
214
214
|
the instance is computed on demand when it is read. Reach for it when a
|
|
215
215
|
command-side read needs the model current at the moment it is read. Passive is
|
|
216
216
|
fresher, not safer: a passive reducer read captures no position and guards nothing,
|
|
217
|
-
and a
|
|
218
|
-
|
|
219
|
-
|
|
217
|
+
and a plain read of a reducer guards nothing. An unfiltered reducer without
|
|
218
|
+
constructor dependencies (other than `ILogger<T>`; self-reducing models are exempt)
|
|
219
|
+
is admitted by decision reads and by Arc read protection `On` (Chronicle 19.68.0);
|
|
220
|
+
a filtered reducer or one with dependencies is refused, and no reducer is admitted in
|
|
221
|
+
a stream-scoped read. For a rule that must hold under concurrent writers read an
|
|
222
|
+
admitted model under `On`; see
|
|
220
223
|
[choosing how a rule holds](../cratis-chronicle-decision-reads/references/choosing-how-a-rule-holds.md).
|
|
221
224
|
|
|
222
225
|
## Discovery, upgrades and ownership
|
|
@@ -81,10 +81,13 @@ there is nothing to review under a red build.
|
|
|
81
81
|
- Logging lives in a `*Logging.cs` partial with `[LoggerMessage]`, not inline in
|
|
82
82
|
domain code.
|
|
83
83
|
- No shared mutable state between commands.
|
|
84
|
-
-
|
|
85
|
-
|
|
84
|
+
- Read protection is `Off` by default; new code sets `<CratisReadProtection>On</CratisReadProtection>`.
|
|
85
|
+
Under `Off` an injected model or validator read is advisory, not a concurrency guard.
|
|
86
|
+
Under `On`, review that state rules sit in validators or handlers with the model as
|
|
87
|
+
a parameter, `[Unprotected("why")]` carries a real reason, and a conflict spec
|
|
88
|
+
(`AppendConcurrently` plus `ConcurrencyViolation`) exists.
|
|
86
89
|
- Use `Cratis.Arc.Core.CodeAnalysis` and `Cratis.Arc.Chronicle.CodeAnalysis`.
|
|
87
|
-
ARCCHR0011
|
|
90
|
+
ARCCHR0011 (Info, `Off` only) and 0012 flag unprotected reads/immediate
|
|
88
91
|
bypasses; 0013 (Info) recommends definitions; 0014 checks declared observer
|
|
89
92
|
streams; 0015 warns on nullable events; 0016 flags ignored concurrency flags;
|
|
90
93
|
0017/0018 check stream-id templates; 0019 warns on folds exposed as queries.
|