@toa.io/atomicity 1.0.0-alpha.284 → 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 +3 -3
- package/src/atom.js +11 -11
- package/src/connection.js +17 -8
- package/src/factory.js +2 -2
- package/src/meter.js +6 -4
- package/CHANGELOG.md +0 -96
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
|
@@ -62,7 +62,7 @@ Uses [redlock](https://github.com/sesamecare/redlock). The key is written to eve
|
|
|
62
62
|
majority holding it is the lock, so a minority can be lost or failed over without invalidating one.
|
|
63
63
|
Against a single address there is no majority to lose: a restart that drops the key, or a failover
|
|
64
64
|
to a replica that has not received it, can leave two holders. Entity writes do not rest on either —
|
|
65
|
-
they have `
|
|
65
|
+
they have `VERSION`.
|
|
66
66
|
|
|
67
67
|
The routine is given an `AbortSignal` and a context. Extension can fail while the routine runs, and
|
|
68
68
|
the signal is how it says so:
|
|
@@ -92,8 +92,8 @@ Requires Redis.
|
|
|
92
92
|
```yaml
|
|
93
93
|
# context.toa.yaml
|
|
94
94
|
atomicity:
|
|
95
|
-
redis: redis://redis.example.com
|
|
96
|
-
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
|
|
97
97
|
```
|
|
98
98
|
|
|
99
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
|
|
@@ -27,7 +27,7 @@ export class Atom extends Connector {
|
|
|
27
27
|
/** the loop's own logger, carrying the group every line belongs to */
|
|
28
28
|
#console
|
|
29
29
|
|
|
30
|
-
constructor
|
|
30
|
+
constructor(connection, name, interval) {
|
|
31
31
|
super()
|
|
32
32
|
|
|
33
33
|
this.#connection = connection
|
|
@@ -50,7 +50,7 @@ export class Atom extends Connector {
|
|
|
50
50
|
* @param total {number}
|
|
51
51
|
* @returns {number[] | null} null while this replica owns nothing
|
|
52
52
|
*/
|
|
53
|
-
slots
|
|
53
|
+
slots(total) {
|
|
54
54
|
const assignment = this.#assignment
|
|
55
55
|
|
|
56
56
|
if (assignment === null) return null
|
|
@@ -78,7 +78,7 @@ export class Atom extends Connector {
|
|
|
78
78
|
* @param listener {(assignment: { i: number, n: number } | null) => void}
|
|
79
79
|
* @returns {() => void}
|
|
80
80
|
*/
|
|
81
|
-
onassigned
|
|
81
|
+
onassigned(listener) {
|
|
82
82
|
this.#listeners.add(listener)
|
|
83
83
|
|
|
84
84
|
listener(this.#assignment)
|
|
@@ -95,7 +95,7 @@ export class Atom extends Connector {
|
|
|
95
95
|
* @param {number[]} deltas
|
|
96
96
|
* @returns {Promise<number[]>}
|
|
97
97
|
*/
|
|
98
|
-
async meter
|
|
98
|
+
async meter(keys, deltas) {
|
|
99
99
|
const meter = this.#connection.meter
|
|
100
100
|
|
|
101
101
|
if (meter === undefined)
|
|
@@ -112,7 +112,7 @@ export class Atom extends Connector {
|
|
|
112
112
|
* @param {() => Promise<any>} routine
|
|
113
113
|
* @returns {Promise<any>}
|
|
114
114
|
*/
|
|
115
|
-
async lock
|
|
115
|
+
async lock(keys, routine) {
|
|
116
116
|
const redlock = this.#connection.redlock
|
|
117
117
|
|
|
118
118
|
if (redlock === undefined)
|
|
@@ -128,13 +128,13 @@ export class Atom extends Connector {
|
|
|
128
128
|
*
|
|
129
129
|
* @private
|
|
130
130
|
*/
|
|
131
|
-
#keys
|
|
131
|
+
#keys(kind, keys) {
|
|
132
132
|
if (typeof keys === 'string') keys = [keys]
|
|
133
133
|
|
|
134
134
|
return keys.map((key) => `${ATOM}:${this.#name}:${kind}:${key}`)
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
async open
|
|
137
|
+
async open() {
|
|
138
138
|
const redis = this.#connection.redis
|
|
139
139
|
|
|
140
140
|
// without a Redis nothing can be owned exclusively, so nothing is claimed at all
|
|
@@ -144,7 +144,7 @@ export class Atom extends Connector {
|
|
|
144
144
|
this.#discovering = this.#discover(redis)
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
async close
|
|
147
|
+
async close() {
|
|
148
148
|
this.#assignment = null
|
|
149
149
|
|
|
150
150
|
this.#abort?.abort()
|
|
@@ -160,7 +160,7 @@ export class Atom extends Connector {
|
|
|
160
160
|
*
|
|
161
161
|
* @private
|
|
162
162
|
*/
|
|
163
|
-
async #discover
|
|
163
|
+
async #discover(redis) {
|
|
164
164
|
const { discover } = await import('n-and-i')
|
|
165
165
|
|
|
166
166
|
const loop = discover({
|
|
@@ -191,7 +191,7 @@ export class Atom extends Connector {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
function override
|
|
194
|
+
function override() {
|
|
195
195
|
const value = Number(process.env.TOA_ATOMICITY_INTERVAL)
|
|
196
196
|
|
|
197
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,96 +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.284](https://github.com/toa-io/toa/compare/v1.0.0-alpha.283...v1.0.0-alpha.284) (2026-09-05)
|
|
7
|
-
|
|
8
|
-
### Features
|
|
9
|
-
|
|
10
|
-
* **atomicity:** a replica can be told what it owns, not only ask ([f7b56b9](https://github.com/toa-io/toa/commit/f7b56b99cc43313f3f855df658a80fdc0659689e))
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# [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)
|
|
14
|
-
|
|
15
|
-
**Note:** Version bump only for package @toa.io/atomicity
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# [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)
|
|
22
|
-
|
|
23
|
-
### Bug Fixes
|
|
24
|
-
|
|
25
|
-
* **atomicity:** nest every key under atom:<group>:<kind> ([348f39f](https://github.com/toa-io/toa/commit/348f39f72d805a24dd4fdbaa83aede47fe95b894))
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# [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)
|
|
29
|
-
|
|
30
|
-
**Note:** Version bump only for package @toa.io/atomicity
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# [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)
|
|
37
|
-
|
|
38
|
-
**Note:** Version bump only for package @toa.io/atomicity
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# [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)
|
|
45
|
-
|
|
46
|
-
**Note:** Version bump only for package @toa.io/atomicity
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# [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)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
### Bug Fixes
|
|
56
|
-
|
|
57
|
-
* **atomicity:** honour a cluster, and let it be unreachable ([c475d66](https://github.com/toa-io/toa/commit/c475d663ddf7329d86da517ccceee4cb03280e95))
|
|
58
|
-
* **atomicity:** stop swallowing faults from the discovery loop ([449f79a](https://github.com/toa-io/toa/commit/449f79a011611f2e14ad5a325a9f7f7161433060))
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* feat(atomicity)!: take a quorum of independent servers ([862562f](https://github.com/toa-io/toa/commit/862562f24d77ec00127dbbd88d43802b208c643b))
|
|
62
|
-
* refactor(atomicity)!: take one Redis, not a cluster ([b0a4770](https://github.com/toa-io/toa/commit/b0a4770dff5dfe8fee4b9ae637a4dd297454fcd4))
|
|
63
|
-
* refactor(atomicity)!: take the lock manager from the stash ([4fd91eb](https://github.com/toa-io/toa/commit/4fd91eb2fdb68f7b87aaf16d182794bbc567ba57))
|
|
64
|
-
* refactor(atomicity)!: make the connector a family, not a partitioner ([06fd63a](https://github.com/toa-io/toa/commit/06fd63ad6296b724ef83afccf4e4e25d0bcaf080))
|
|
65
|
-
* refactor(atomicity)!: rename the connector and free it of the outbox ([21f1a41](https://github.com/toa-io/toa/commit/21f1a41aceafcfd35c7620014062fe46b54afa95))
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
### Features
|
|
69
|
-
|
|
70
|
-
* **atomicity:** make the interval a setting, and stop repeating n-and-i ([5c3a6d1](https://github.com/toa-io/toa/commit/5c3a6d1533751a21b7eb7d9c737f1a270bf5a5ae))
|
|
71
|
-
* **atomicity:** meter what the group has spent ([bb1118a](https://github.com/toa-io/toa/commit/bb1118aa60bfeb43d3212f2495b7797445f003c3))
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
### BREAKING CHANGES
|
|
75
|
-
|
|
76
|
-
* `atomicity.redis` accepts a list again, of independent servers rather
|
|
77
|
-
than cluster nodes, and refuses an even number of them.
|
|
78
|
-
|
|
79
|
-
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
80
|
-
* `atomicity.redis` is a string. A list is refused at export.
|
|
81
|
-
|
|
82
|
-
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
83
|
-
* `context.stash.lock` is gone; lock through `context.atom` instead.
|
|
84
|
-
A stash pointer resolving to several addresses now uses the first.
|
|
85
|
-
|
|
86
|
-
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
87
|
-
* `Factory.partition(group)` is `Factory.atom(group)`, and the
|
|
88
|
-
`Partition` it returned is an `Atom`. `slots(total)` is unchanged.
|
|
89
|
-
|
|
90
|
-
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
91
|
-
* `@toa.io/partitions.redis` is now `@toa.io/atomicity`, and its
|
|
92
|
-
`lanes(total)` is `slots(total)`. Redis is declared as `atomicity` in the context
|
|
93
|
-
rather than `outbox.redis`, and read from `TOA_ATOMICITY_REDIS`.
|
|
94
|
-
`TOA_OUTBOX_PARTITION_INTERVAL` is `TOA_ATOMICITY_INTERVAL`.
|
|
95
|
-
|
|
96
|
-
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|