1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
//! Committing with the journal, rather than beside it.
//!
//! # The one case where a saga is not the best available answer
//!
//! An [`EffectGroup`](crate::runtime::EffectGroup) is the saga form: members are
//! performed, and taken back if the group aborts. That is the right answer when
//! the members live in systems that cannot share a transaction — a payment
//! provider and a warehouse — because there is nothing else on offer.
//!
//! It is *not* the best answer when the resource lives in the **same database as
//! the journal**. There the member's write and the record that it happened can
//! commit together, and then:
//!
//! * nothing is externalised and later reversed, so no reversal can fail;
//! * the in-doubt window shrinks to one instant: a transaction either committed
//! or did not, but the *client's knowledge* of which can be lost when the
//! connection drops between `COMMIT` and its acknowledgement. That one case
//! is [`StoreError::CommitUnknown`](crate::core::StoreError::CommitUnknown),
//! and the group quarantines over it rather than aborting — a commit the
//! server *refused* is the clean rollback and takes the cheap abort;
//! * an abort is a `ROLLBACK`, which is free and cannot itself fail halfway.
//!
//! Compensation that never has to run beats compensation that runs correctly.
//!
//! # Why this seam is SQL-shaped, and only Postgres has it
//!
//! The premise is that the resource is *already there* — a ledger table, a
//! reservation table, whatever the deployment keeps beside its journal. So the
//! seam speaks the language that resource is written in. A key-value seam that
//! every backend could implement would only be able to touch a table this crate
//! defined, which is not the table anybody wants to be atomic with.
//!
//! Embedded backends return `None` from
//! [`JournalStore::atomic`](crate::journal::JournalStore::atomic). That is a
//! capability being absent rather than a failure: a group with an atomic member
//! on a store that cannot enlist is refused when the member is registered, which
//! is the only time refusing is free.
//!
//! # The work returns the records
//!
//! [`AtomicWork::run`] both applies the members and hands back the records to
//! append. This is not a convenience: the records carry the members' *outputs*,
//! so anything that built them outside the transaction would be describing work
//! whose result it could not yet know. One call, one transaction, no ordering
//! for a caller to get wrong.
use Debug;
use async_trait;
use Value;
use crate;
use crate;
/// A value a co-located resource binds into a statement.
///
/// Deliberately a closed set rather than the driver's own parameter trait. A
/// resource written against `tokio_postgres::ToSql` would be a resource that
/// cannot be tested without a database and cannot move to another backend, and
/// this crate would have a driver in its public API.
/// As much of the journal's own transaction as a co-located resource may use.
///
/// Statements are parameterised because the alternative is a resource building
/// SQL by concatenation, and a resource is handed values that came from a model
/// often enough that this is not a style preference.
/// One member that commits with the journal or not at all.
/// What runs inside the journal's transaction.
///
/// Implemented by the runtime, not by users: it is how an
/// [`EffectGroup`](crate::runtime::EffectGroup) hands its atomic members and the
/// records describing them to the store as one unit.
/// A store whose own transaction a co-located resource can join.
///
/// Reached through [`JournalStore::atomic`](crate::journal::JournalStore::atomic),
/// which answers `None` for a backend that cannot offer this. Absence is the
/// honest answer and it is checked where it is cheap — at registration, not at
/// commit.