@toa.io/atomicity 1.0.0-alpha.283 → 1.0.0-alpha.286
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.json +4 -4
- package/readme.md +23 -3
- package/src/atom.js +52 -13
- package/src/connection.js +17 -8
- package/src/factory.js +2 -2
- package/src/meter.js +6 -4
- package/CHANGELOG.md +0 -89
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/atomicity",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.286",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Toa Atomicity Connector",
|
|
6
6
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@sesamecare-oss/redlock": "1.4.0",
|
|
21
|
-
"@toa.io/core": "1.0.0-alpha.
|
|
21
|
+
"@toa.io/core": "1.0.0-alpha.286",
|
|
22
22
|
"ioredis": "6.0.0",
|
|
23
23
|
"n-and-i": "0.3.2",
|
|
24
|
-
"openspan": "1.0.0-alpha.
|
|
24
|
+
"openspan": "1.0.0-alpha.286"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
28
28
|
},
|
|
29
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "81f3d0688f17668b902be336d1749cc715edcdb6"
|
|
30
30
|
}
|
package/readme.md
CHANGED
|
@@ -18,6 +18,26 @@ Returns `null` while this replica owns nothing — at startup, during a rollout,
|
|
|
18
18
|
|
|
19
19
|
Uses [n-and-i](https://github.com/temich/nandi).
|
|
20
20
|
|
|
21
|
+
### Being told
|
|
22
|
+
|
|
23
|
+
`slots` answers whoever asks. A consumer that asks rarely — once an hour, say — samples one
|
|
24
|
+
moment, and a rollout passing through that moment costs it the whole hour. `onassigned` tells it
|
|
25
|
+
instead.
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
const off = atom.onassigned((assignment) => { … }) // { i, n } — or null, owning nothing
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Called with the assignment this replica holds, and again whenever it changes: one arrived, was
|
|
32
|
+
lost, or the group resized. A change and not a heartbeat — a group that stays as it is never
|
|
33
|
+
calls back. It is called once as it is added, with the assignment as it stands, so a listener
|
|
34
|
+
arriving after the group settled is not left waiting for something that has already happened.
|
|
35
|
+
|
|
36
|
+
The answer removes the listener. A listener that raises ends the loop it was told through, and
|
|
37
|
+
that loop is what keeps this replica registered — so this replica owns nothing from that moment,
|
|
38
|
+
`slots` answers `null`, and it stands down. What to do about the exception is the runtime's;
|
|
39
|
+
nothing here catches it.
|
|
40
|
+
|
|
21
41
|
## Metering
|
|
22
42
|
|
|
23
43
|
Debt the group has run up under each key, in milliseconds. A call adds its own deltas and answers
|
|
@@ -42,7 +62,7 @@ Uses [redlock](https://github.com/sesamecare/redlock). The key is written to eve
|
|
|
42
62
|
majority holding it is the lock, so a minority can be lost or failed over without invalidating one.
|
|
43
63
|
Against a single address there is no majority to lose: a restart that drops the key, or a failover
|
|
44
64
|
to a replica that has not received it, can leave two holders. Entity writes do not rest on either —
|
|
45
|
-
they have `
|
|
65
|
+
they have `VERSION`.
|
|
46
66
|
|
|
47
67
|
The routine is given an `AbortSignal` and a context. Extension can fail while the routine runs, and
|
|
48
68
|
the signal is how it says so:
|
|
@@ -72,8 +92,8 @@ Requires Redis.
|
|
|
72
92
|
```yaml
|
|
73
93
|
# context.toa.yaml
|
|
74
94
|
atomicity:
|
|
75
|
-
redis: redis://redis.example.com
|
|
76
|
-
interval: 5000
|
|
95
|
+
redis: redis://redis.example.com # one address, or an odd number of them
|
|
96
|
+
interval: 5000 # how often a replica registers, milliseconds
|
|
77
97
|
```
|
|
78
98
|
|
|
79
99
|
Several addresses are **independent servers**, not the nodes of a cluster. The lock is taken on a
|
package/src/atom.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Connector } from '@toa.io/core'
|
|
|
5
5
|
* What one group of replicas decides together, in one place: which of them owns what, what they
|
|
6
6
|
* have spent between them, and which of them holds a name while it works.
|
|
7
7
|
*
|
|
8
|
-
* @implements {toa.core.atomicity.Atom}
|
|
8
|
+
* @implements {import('@toa.io/core/types').atomicity.Atom}
|
|
9
9
|
*/
|
|
10
10
|
export class Atom extends Connector {
|
|
11
11
|
#connection
|
|
@@ -15,6 +15,9 @@ export class Atom extends Connector {
|
|
|
15
15
|
/** @type {{ i: number, n: number } | null} */
|
|
16
16
|
#assignment = null
|
|
17
17
|
|
|
18
|
+
/** @type {Set<(assignment: { i: number, n: number } | null) => void>} */
|
|
19
|
+
#listeners = new Set()
|
|
20
|
+
|
|
18
21
|
/** @type {AbortController} */
|
|
19
22
|
#abort
|
|
20
23
|
|
|
@@ -24,7 +27,7 @@ export class Atom extends Connector {
|
|
|
24
27
|
/** the loop's own logger, carrying the group every line belongs to */
|
|
25
28
|
#console
|
|
26
29
|
|
|
27
|
-
constructor
|
|
30
|
+
constructor(connection, name, interval) {
|
|
28
31
|
super()
|
|
29
32
|
|
|
30
33
|
this.#connection = connection
|
|
@@ -47,7 +50,7 @@ export class Atom extends Connector {
|
|
|
47
50
|
* @param total {number}
|
|
48
51
|
* @returns {number[] | null} null while this replica owns nothing
|
|
49
52
|
*/
|
|
50
|
-
slots
|
|
53
|
+
slots(total) {
|
|
51
54
|
const assignment = this.#assignment
|
|
52
55
|
|
|
53
56
|
if (assignment === null) return null
|
|
@@ -60,6 +63,29 @@ export class Atom extends Connector {
|
|
|
60
63
|
return owned
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Calls `listener` with the assignment this replica holds, and again whenever it changes —
|
|
68
|
+
* one arrived, was lost, or the group resized. Answers with what removes the listener again.
|
|
69
|
+
*
|
|
70
|
+
* A change and not a heartbeat: a group that stays as it is never calls back. It is called
|
|
71
|
+
* once as it is added, with the claim as it stands, so that a listener arriving after the
|
|
72
|
+
* group settled is not left waiting for something that has already happened.
|
|
73
|
+
*
|
|
74
|
+
* This is what `slots` cannot say. Reading answers a consumer that asks often, where a stale
|
|
75
|
+
* answer costs one cycle; a consumer that asks rarely needs to be told, or it samples the
|
|
76
|
+
* one moment a rollout was passing through and stands down for a whole cycle.
|
|
77
|
+
*
|
|
78
|
+
* @param listener {(assignment: { i: number, n: number } | null) => void}
|
|
79
|
+
* @returns {() => void}
|
|
80
|
+
*/
|
|
81
|
+
onassigned(listener) {
|
|
82
|
+
this.#listeners.add(listener)
|
|
83
|
+
|
|
84
|
+
listener(this.#assignment)
|
|
85
|
+
|
|
86
|
+
return () => this.#listeners.delete(listener)
|
|
87
|
+
}
|
|
88
|
+
|
|
63
89
|
/**
|
|
64
90
|
* Debt the group has run up under each key, in milliseconds: every call adds its own deltas
|
|
65
91
|
* and reads back where the group stands, so a replica reports only what it has spent and
|
|
@@ -69,7 +95,7 @@ export class Atom extends Connector {
|
|
|
69
95
|
* @param {number[]} deltas
|
|
70
96
|
* @returns {Promise<number[]>}
|
|
71
97
|
*/
|
|
72
|
-
async meter
|
|
98
|
+
async meter(keys, deltas) {
|
|
73
99
|
const meter = this.#connection.meter
|
|
74
100
|
|
|
75
101
|
if (meter === undefined)
|
|
@@ -86,7 +112,7 @@ export class Atom extends Connector {
|
|
|
86
112
|
* @param {() => Promise<any>} routine
|
|
87
113
|
* @returns {Promise<any>}
|
|
88
114
|
*/
|
|
89
|
-
async lock
|
|
115
|
+
async lock(keys, routine) {
|
|
90
116
|
const redlock = this.#connection.redlock
|
|
91
117
|
|
|
92
118
|
if (redlock === undefined)
|
|
@@ -102,13 +128,13 @@ export class Atom extends Connector {
|
|
|
102
128
|
*
|
|
103
129
|
* @private
|
|
104
130
|
*/
|
|
105
|
-
#keys
|
|
131
|
+
#keys(kind, keys) {
|
|
106
132
|
if (typeof keys === 'string') keys = [keys]
|
|
107
133
|
|
|
108
134
|
return keys.map((key) => `${ATOM}:${this.#name}:${kind}:${key}`)
|
|
109
135
|
}
|
|
110
136
|
|
|
111
|
-
async open
|
|
137
|
+
async open() {
|
|
112
138
|
const redis = this.#connection.redis
|
|
113
139
|
|
|
114
140
|
// without a Redis nothing can be owned exclusively, so nothing is claimed at all
|
|
@@ -118,7 +144,7 @@ export class Atom extends Connector {
|
|
|
118
144
|
this.#discovering = this.#discover(redis)
|
|
119
145
|
}
|
|
120
146
|
|
|
121
|
-
async close
|
|
147
|
+
async close() {
|
|
122
148
|
this.#assignment = null
|
|
123
149
|
|
|
124
150
|
this.#abort?.abort()
|
|
@@ -134,7 +160,7 @@ export class Atom extends Connector {
|
|
|
134
160
|
*
|
|
135
161
|
* @private
|
|
136
162
|
*/
|
|
137
|
-
async #discover
|
|
163
|
+
async #discover(redis) {
|
|
138
164
|
const { discover } = await import('n-and-i')
|
|
139
165
|
|
|
140
166
|
const loop = discover({
|
|
@@ -146,13 +172,26 @@ export class Atom extends Connector {
|
|
|
146
172
|
console: this.#console
|
|
147
173
|
})
|
|
148
174
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
175
|
+
try {
|
|
176
|
+
// the loop yields when ownership changes, which is exactly when work has to be handed over
|
|
177
|
+
for await (const { i, n } of loop) {
|
|
178
|
+
this.#assignment = i === null ? null : { i, n }
|
|
179
|
+
|
|
180
|
+
for (const listener of this.#listeners) listener(this.#assignment)
|
|
181
|
+
}
|
|
182
|
+
} finally {
|
|
183
|
+
/*
|
|
184
|
+
* The loop is what keeps this replica registered, so once it has ended — because it was
|
|
185
|
+
* aborted, or because a listener raised through it — this replica owns nothing. Left as
|
|
186
|
+
* it was, the last assignment would answer `slots` for as long as the process ran, and
|
|
187
|
+
* a claim nothing supports is what the whole of this exists to prevent.
|
|
188
|
+
*/
|
|
189
|
+
this.#assignment = null
|
|
190
|
+
}
|
|
152
191
|
}
|
|
153
192
|
}
|
|
154
193
|
|
|
155
|
-
function override
|
|
194
|
+
function override() {
|
|
156
195
|
const value = Number(process.env.TOA_ATOMICITY_INTERVAL)
|
|
157
196
|
|
|
158
197
|
return Number.isNaN(value) || value <= 0 ? undefined : value
|
package/src/connection.js
CHANGED
|
@@ -26,12 +26,14 @@ export class Connection extends Connector {
|
|
|
26
26
|
/** @type {import('ioredis').Redis[]} */
|
|
27
27
|
#clients = []
|
|
28
28
|
|
|
29
|
-
async open
|
|
29
|
+
async open() {
|
|
30
30
|
const urls = resolve()
|
|
31
31
|
|
|
32
32
|
if (urls.length === 0) {
|
|
33
|
-
console.warn(
|
|
34
|
-
'
|
|
33
|
+
console.warn(
|
|
34
|
+
'Atomicity is not configured, so nothing is owned and nothing is metered. ' +
|
|
35
|
+
'Set TOA_ATOMICITY_REDIS.'
|
|
36
|
+
)
|
|
35
37
|
|
|
36
38
|
return
|
|
37
39
|
}
|
|
@@ -56,8 +58,11 @@ export class Connection extends Connector {
|
|
|
56
58
|
for (const client of this.#clients)
|
|
57
59
|
// ioredis leaves `message` empty on a refused connection, where the code is the whole story
|
|
58
60
|
client.on('error', (error) =>
|
|
59
|
-
console.warn('Atomicity is unreachable, so nothing is owned',
|
|
60
|
-
|
|
61
|
+
console.warn('Atomicity is unreachable, so nothing is owned', {
|
|
62
|
+
host: client.options.host,
|
|
63
|
+
error: error.code ?? error.message
|
|
64
|
+
})
|
|
65
|
+
)
|
|
61
66
|
|
|
62
67
|
this.redis = this.#clients[0]
|
|
63
68
|
|
|
@@ -75,7 +80,7 @@ export class Connection extends Connector {
|
|
|
75
80
|
console.info('Atomicity connecting to redis', { nodes: this.#clients.length })
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
async close
|
|
83
|
+
async close() {
|
|
79
84
|
for (const client of this.#clients) client.disconnect()
|
|
80
85
|
|
|
81
86
|
// a closed connection holds nothing: it is opened again with whatever is configured then,
|
|
@@ -89,7 +94,7 @@ export class Connection extends Connector {
|
|
|
89
94
|
|
|
90
95
|
const OPTIONS = { enableReadyCheck: true }
|
|
91
96
|
|
|
92
|
-
function resolve
|
|
97
|
+
function resolve() {
|
|
93
98
|
const value = process.env[VARIABLE]
|
|
94
99
|
|
|
95
100
|
// an empty value is atomicity turned off, where an absent one in development is the
|
|
@@ -108,4 +113,8 @@ export const connection = () => (instance ??= new Connection())
|
|
|
108
113
|
export const reset = () => (instance = undefined)
|
|
109
114
|
|
|
110
115
|
export const VARIABLE = 'TOA_ATOMICITY_REDIS'
|
|
111
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Toa's own development stack is not on the conventional ports: the applications built on
|
|
118
|
+
* Toa are, and they share the machine. See CONTRIBUTING.md.
|
|
119
|
+
*/
|
|
120
|
+
const DEV = 'redis://localhost:31040'
|
package/src/factory.js
CHANGED
|
@@ -9,7 +9,7 @@ export class Factory {
|
|
|
9
9
|
* @param {string} group
|
|
10
10
|
* @param {object} [options]
|
|
11
11
|
*/
|
|
12
|
-
atom
|
|
12
|
+
atom(group, options = {}) {
|
|
13
13
|
return atom(group, options)
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -22,7 +22,7 @@ const atoms = new Map()
|
|
|
22
22
|
* time in a group that has one replica, and every replica of that group would then be told it
|
|
23
23
|
* owns half of what it does.
|
|
24
24
|
*/
|
|
25
|
-
function atom
|
|
25
|
+
function atom(group, options) {
|
|
26
26
|
let atom = atoms.get(group)
|
|
27
27
|
|
|
28
28
|
if (atom === undefined) {
|
package/src/meter.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
export class Meter {
|
|
16
16
|
#redis
|
|
17
17
|
|
|
18
|
-
constructor
|
|
18
|
+
constructor(redis) {
|
|
19
19
|
this.#redis = redis
|
|
20
20
|
|
|
21
21
|
redis.defineCommand(COMMAND, { lua: SCRIPT })
|
|
@@ -28,7 +28,7 @@ export class Meter {
|
|
|
28
28
|
* @param {number[]} deltas
|
|
29
29
|
* @returns {Promise<number[]>}
|
|
30
30
|
*/
|
|
31
|
-
async meter
|
|
31
|
+
async meter(keys, deltas) {
|
|
32
32
|
if (keys.length <= CHUNK) return this.#call(keys, deltas)
|
|
33
33
|
|
|
34
34
|
// arguments are spread into the call, so a batch is split rather than risking the
|
|
@@ -36,13 +36,15 @@ export class Meter {
|
|
|
36
36
|
const debts = []
|
|
37
37
|
|
|
38
38
|
for (let i = 0; i < keys.length; i += CHUNK)
|
|
39
|
-
debts.push(
|
|
39
|
+
debts.push(
|
|
40
|
+
...(await this.#call(keys.slice(i, i + CHUNK), deltas.slice(i, i + CHUNK)))
|
|
41
|
+
)
|
|
40
42
|
|
|
41
43
|
return debts
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
/** @private */
|
|
45
|
-
async #call
|
|
47
|
+
async #call(keys, deltas) {
|
|
46
48
|
// `defineCommand` extends the client at runtime
|
|
47
49
|
return this.#redis[COMMAND](keys.length, ...keys, ...deltas)
|
|
48
50
|
}
|
package/CHANGELOG.md
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# Change Log
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
-
|
|
6
|
-
# [1.0.0-alpha.283](https://github.com/toa-io/toa/compare/v1.0.0-alpha.282...v1.0.0-alpha.283) (2026-09-04)
|
|
7
|
-
|
|
8
|
-
**Note:** Version bump only for package @toa.io/atomicity
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# [1.0.0-alpha.282](https://github.com/toa-io/toa/compare/v1.0.0-alpha.281...v1.0.0-alpha.282) (2026-09-03)
|
|
15
|
-
|
|
16
|
-
### Bug Fixes
|
|
17
|
-
|
|
18
|
-
* **atomicity:** nest every key under atom:<group>:<kind> ([348f39f](https://github.com/toa-io/toa/commit/348f39f72d805a24dd4fdbaa83aede47fe95b894))
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# [1.0.0-alpha.278](https://github.com/toa-io/toa/compare/v1.0.0-alpha.277...v1.0.0-alpha.278) (2026-09-03)
|
|
22
|
-
|
|
23
|
-
**Note:** Version bump only for package @toa.io/atomicity
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# [1.0.0-alpha.277](https://github.com/toa-io/toa/compare/v1.0.0-alpha.276...v1.0.0-alpha.277) (2026-09-03)
|
|
30
|
-
|
|
31
|
-
**Note:** Version bump only for package @toa.io/atomicity
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# [1.0.0-alpha.273](https://github.com/toa-io/toa/compare/v1.0.0-alpha.272...v1.0.0-alpha.273) (2026-09-02)
|
|
38
|
-
|
|
39
|
-
**Note:** Version bump only for package @toa.io/atomicity
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
# [1.0.0-alpha.272](https://github.com/toa-io/toa/compare/v1.0.0-alpha.271...v1.0.0-alpha.272) (2026-09-01)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
### Bug Fixes
|
|
49
|
-
|
|
50
|
-
* **atomicity:** honour a cluster, and let it be unreachable ([c475d66](https://github.com/toa-io/toa/commit/c475d663ddf7329d86da517ccceee4cb03280e95))
|
|
51
|
-
* **atomicity:** stop swallowing faults from the discovery loop ([449f79a](https://github.com/toa-io/toa/commit/449f79a011611f2e14ad5a325a9f7f7161433060))
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* feat(atomicity)!: take a quorum of independent servers ([862562f](https://github.com/toa-io/toa/commit/862562f24d77ec00127dbbd88d43802b208c643b))
|
|
55
|
-
* refactor(atomicity)!: take one Redis, not a cluster ([b0a4770](https://github.com/toa-io/toa/commit/b0a4770dff5dfe8fee4b9ae637a4dd297454fcd4))
|
|
56
|
-
* refactor(atomicity)!: take the lock manager from the stash ([4fd91eb](https://github.com/toa-io/toa/commit/4fd91eb2fdb68f7b87aaf16d182794bbc567ba57))
|
|
57
|
-
* refactor(atomicity)!: make the connector a family, not a partitioner ([06fd63a](https://github.com/toa-io/toa/commit/06fd63ad6296b724ef83afccf4e4e25d0bcaf080))
|
|
58
|
-
* refactor(atomicity)!: rename the connector and free it of the outbox ([21f1a41](https://github.com/toa-io/toa/commit/21f1a41aceafcfd35c7620014062fe46b54afa95))
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
### Features
|
|
62
|
-
|
|
63
|
-
* **atomicity:** make the interval a setting, and stop repeating n-and-i ([5c3a6d1](https://github.com/toa-io/toa/commit/5c3a6d1533751a21b7eb7d9c737f1a270bf5a5ae))
|
|
64
|
-
* **atomicity:** meter what the group has spent ([bb1118a](https://github.com/toa-io/toa/commit/bb1118aa60bfeb43d3212f2495b7797445f003c3))
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
### BREAKING CHANGES
|
|
68
|
-
|
|
69
|
-
* `atomicity.redis` accepts a list again, of independent servers rather
|
|
70
|
-
than cluster nodes, and refuses an even number of them.
|
|
71
|
-
|
|
72
|
-
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
73
|
-
* `atomicity.redis` is a string. A list is refused at export.
|
|
74
|
-
|
|
75
|
-
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
76
|
-
* `context.stash.lock` is gone; lock through `context.atom` instead.
|
|
77
|
-
A stash pointer resolving to several addresses now uses the first.
|
|
78
|
-
|
|
79
|
-
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
80
|
-
* `Factory.partition(group)` is `Factory.atom(group)`, and the
|
|
81
|
-
`Partition` it returned is an `Atom`. `slots(total)` is unchanged.
|
|
82
|
-
|
|
83
|
-
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
84
|
-
* `@toa.io/partitions.redis` is now `@toa.io/atomicity`, and its
|
|
85
|
-
`lanes(total)` is `slots(total)`. Redis is declared as `atomicity` in the context
|
|
86
|
-
rather than `outbox.redis`, and read from `TOA_ATOMICITY_REDIS`.
|
|
87
|
-
`TOA_OUTBOX_PARTITION_INTERVAL` is `TOA_ATOMICITY_INTERVAL`.
|
|
88
|
-
|
|
89
|
-
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|