@rbxts/dataforge 0.1.0 → 0.2.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/index.d.ts +218 -26
- package/package.json +1 -1
- package/src/errors.luau +195 -0
- package/src/hooks/memory.luau +1 -12
- package/src/hooks/roblox.luau +10 -1
- package/src/init.luau +6 -2
- package/src/mutex.luau +5 -5
- package/src/profile/common.luau +41 -8
- package/src/profile/lockless.luau +102 -50
- package/src/profile/peek.luau +22 -16
- package/src/profile/profile.luau +156 -95
- package/src/result.luau +61 -0
- package/src/store.luau +89 -41
- package/src/tx/handle.luau +82 -59
- package/src/tx/operation.luau +6 -3
- package/src/types.luau +187 -16
- package/src/utils.luau +37 -3
package/src/store.luau
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
--! strict
|
|
2
2
|
|
|
3
|
+
local errors = require "./errors"
|
|
4
|
+
local result = require "./result"
|
|
3
5
|
local types = require "./types"
|
|
4
6
|
local utils = require "./utils"
|
|
5
7
|
local roblox_scheduler = require "./schedulers/roblox"
|
|
@@ -20,6 +22,7 @@ type Profile<T = any> = types.Profile<T>
|
|
|
20
22
|
type LocklessProfile<T = any> = types.LocklessProfile<T>
|
|
21
23
|
type PeekProfile<T = any> = types.PeekProfile<T>
|
|
22
24
|
type HooksList<T... = ()> = types.HooksList<T...>
|
|
25
|
+
type Result<T> = types.Result<T>
|
|
23
26
|
|
|
24
27
|
export type Store<T = any> = {
|
|
25
28
|
_loading: { [string]: boolean },
|
|
@@ -43,30 +46,38 @@ export type Store<T = any> = {
|
|
|
43
46
|
},
|
|
44
47
|
|
|
45
48
|
config: ResolvedConfig<T>,
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
--- Takes the session lock for `key` and loads it. Yields. Fails with
|
|
50
|
+
--- `store_closed`, `already_loaded`, `timeout`, `roblox`, `outdated` (a
|
|
51
|
+
--- newer server wrote the record; no lock is taken), `migration_mismatch`
|
|
52
|
+
--- or `tx_marker_invalid`.
|
|
53
|
+
load: (self: Store<T>, key: string, user_ids: { number }) -> Result<Profile<T>>,
|
|
54
|
+
--- Returns the profile for `key`, waiting for it to load or loading it if
|
|
55
|
+
--- nobody has yet. Waiters get the same Result as the load.
|
|
56
|
+
wait_loaded: (self: Store<T>, key: string, user_ids: { number }) -> Result<Profile<T>>,
|
|
48
57
|
get_loaded: (self: Store<T>, key: string) -> Profile<T>?,
|
|
49
58
|
|
|
50
59
|
--- Returns the lockless profile for `key`, creating it if needed. Never
|
|
51
|
-
--- yields: a lockless profile does no storage work until `fetch`.
|
|
60
|
+
--- yields: a lockless profile does no storage work until `fetch`. Throws a
|
|
61
|
+
--- `store_closed` error on a closed store.
|
|
52
62
|
get_lockless: (self: Store<T>, key: string, user_ids: { number }) -> LocklessProfile<T>,
|
|
53
63
|
|
|
54
64
|
--- Read-only snapshot of `key` via `GetAsync`: the current data and lock.
|
|
55
65
|
--- Yields. The result is not tracked by the store, takes no lock and cannot
|
|
56
|
-
--- join a transaction.
|
|
57
|
-
|
|
66
|
+
--- join a transaction. Fails with `store_closed`, `roblox`, `outdated` or
|
|
67
|
+
--- `migration_mismatch`.
|
|
68
|
+
peek: (self: Store<T>, key: string) -> Result<PeekProfile<T>>,
|
|
58
69
|
|
|
59
70
|
--- Atomically updates profiles of this store (locked or lockless). `transform`
|
|
60
71
|
--- gets their data in order and returns the new data in the same order, or
|
|
61
|
-
--- false to cancel.
|
|
62
|
-
---
|
|
63
|
-
--- hook and scheduler default to the store's.
|
|
72
|
+
--- false to cancel. Ok(false) when cancelled, Ok(true) when committed; fails
|
|
73
|
+
--- like `dataforge.transaction`. `config` fills in what the store does not
|
|
74
|
+
--- know (ttls, retry); hook and scheduler default to the store's.
|
|
64
75
|
transaction: (
|
|
65
76
|
self: Store<T>,
|
|
66
77
|
profiles: Array<ProfileBase<T>>,
|
|
67
78
|
transform: (data: Array<T>) -> Array<T> | false,
|
|
68
79
|
config: types.TransactionConfig?
|
|
69
|
-
) -> boolean
|
|
80
|
+
) -> Result<boolean>,
|
|
70
81
|
|
|
71
82
|
on_change: (self: Store<T>, callback: (profile: Profile<T>, new: T, old: T?) -> ()) -> (),
|
|
72
83
|
on_save: (self: Store<T>, callback: (profile: Profile<T>) -> ()) -> (),
|
|
@@ -77,7 +88,9 @@ export type Store<T = any> = {
|
|
|
77
88
|
close: (self: Store<T>) -> (),
|
|
78
89
|
}
|
|
79
90
|
|
|
80
|
-
|
|
91
|
+
--- Resumes every `wait_loaded` caller with the load's Result, or with a raw
|
|
92
|
+
--- error (`res == nil`) they rethrow.
|
|
93
|
+
local function resume_load_locks(self: Store, key: string, res: Result<Profile>?, raw: any?)
|
|
81
94
|
local loading_locks = self._loading_locks[key]
|
|
82
95
|
if not loading_locks then
|
|
83
96
|
return
|
|
@@ -85,7 +98,7 @@ local function resume_load_locks(self: Store, key: string, ok: boolean, result:
|
|
|
85
98
|
self._loading_locks[key] = nil
|
|
86
99
|
|
|
87
100
|
for _, thread in loading_locks do
|
|
88
|
-
self.config._scheduler.spawn(thread,
|
|
101
|
+
self.config._scheduler.spawn(thread, res, raw)
|
|
89
102
|
end
|
|
90
103
|
end
|
|
91
104
|
|
|
@@ -202,16 +215,16 @@ function store.create<T>(config: Config<T>): Store<T>
|
|
|
202
215
|
return setmetatable(self, store) :: any
|
|
203
216
|
end
|
|
204
217
|
|
|
205
|
-
function store.load(self: Store, key: string, user_ids: { number }): Profile
|
|
218
|
+
function store.load(self: Store, key: string, user_ids: { number }): Result<Profile>
|
|
206
219
|
if self.closed then
|
|
207
|
-
|
|
220
|
+
return result.err(errors.store_closed(self))
|
|
208
221
|
end
|
|
209
222
|
if self.profiles[key] ~= nil or self._loading[key] then
|
|
210
|
-
|
|
223
|
+
return result.err(errors.already_loaded(self, key))
|
|
211
224
|
end
|
|
212
225
|
self._loading[key] = true
|
|
213
226
|
|
|
214
|
-
local ok,
|
|
227
|
+
local ok, loaded = utils.try(profile.load, {
|
|
215
228
|
key = key,
|
|
216
229
|
config = self.config,
|
|
217
230
|
user_ids = user_ids,
|
|
@@ -220,29 +233,46 @@ function store.load(self: Store, key: string, user_ids: { number }): Profile
|
|
|
220
233
|
self._loading[key] = nil
|
|
221
234
|
|
|
222
235
|
if not ok then
|
|
223
|
-
|
|
224
|
-
|
|
236
|
+
if errors.is(loaded) then
|
|
237
|
+
local failure = result.err(loaded)
|
|
238
|
+
resume_load_locks(self, key, failure)
|
|
239
|
+
return failure
|
|
240
|
+
end
|
|
241
|
+
-- a migration threw: everyone waiting gets the raw error too
|
|
242
|
+
resume_load_locks(self, key, nil, loaded)
|
|
243
|
+
error(loaded, 0)
|
|
225
244
|
end
|
|
226
245
|
|
|
227
|
-
|
|
228
|
-
|
|
246
|
+
if self.closed then
|
|
247
|
+
-- the store closed while this load was in flight: give the lock back
|
|
248
|
+
-- right away instead of registering a profile nobody will unload
|
|
249
|
+
loaded:unload()
|
|
250
|
+
local failure = result.err(errors.store_closed(self))
|
|
251
|
+
resume_load_locks(self, key, failure)
|
|
252
|
+
return failure
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
loaded:on_closed(function()
|
|
256
|
+
unregister_profile(self, loaded)
|
|
229
257
|
end)
|
|
230
|
-
forward_hooks(self,
|
|
258
|
+
forward_hooks(self, loaded)
|
|
231
259
|
|
|
232
|
-
self.profiles[key] =
|
|
233
|
-
|
|
234
|
-
|
|
260
|
+
self.profiles[key] = loaded
|
|
261
|
+
local success = result.ok(loaded)
|
|
262
|
+
resume_load_locks(self, key, success)
|
|
263
|
+
return success
|
|
235
264
|
end
|
|
236
265
|
|
|
237
|
-
--- Returns the profile for `key`, waiting for it to load or loading it if
|
|
238
|
-
|
|
266
|
+
--- Returns the profile for `key`, waiting for it to load or loading it if
|
|
267
|
+
--- nobody has yet. Waiters get the same Result (or raw error) as the load.
|
|
268
|
+
function store.wait_loaded(self: Store, key: string, user_ids: { number }): Result<Profile>
|
|
239
269
|
if self.closed then
|
|
240
|
-
|
|
270
|
+
return result.err(errors.store_closed(self))
|
|
241
271
|
end
|
|
242
272
|
|
|
243
273
|
local existing = self.profiles[key]
|
|
244
274
|
if existing ~= nil then
|
|
245
|
-
return existing
|
|
275
|
+
return result.ok(existing)
|
|
246
276
|
end
|
|
247
277
|
|
|
248
278
|
if not self._loading[key] then
|
|
@@ -250,12 +280,11 @@ function store.wait_loaded(self: Store, key: string, user_ids: { number }): Prof
|
|
|
250
280
|
end
|
|
251
281
|
insert_loaded_lock(self, key)
|
|
252
282
|
|
|
253
|
-
local
|
|
254
|
-
if
|
|
255
|
-
error(
|
|
283
|
+
local res, raw = coroutine.yield()
|
|
284
|
+
if res == nil then
|
|
285
|
+
error(raw, 0)
|
|
256
286
|
end
|
|
257
|
-
|
|
258
|
-
return result
|
|
287
|
+
return res
|
|
259
288
|
end
|
|
260
289
|
|
|
261
290
|
function store.get_loaded(self: Store, key: string): Profile?
|
|
@@ -264,7 +293,7 @@ end
|
|
|
264
293
|
|
|
265
294
|
function store.get_lockless(self: Store, key: string, user_ids: { number }): LocklessProfile
|
|
266
295
|
if self.closed then
|
|
267
|
-
error
|
|
296
|
+
error(errors.store_closed(self), 0)
|
|
268
297
|
end
|
|
269
298
|
|
|
270
299
|
local existing = self.lockless_profiles[key]
|
|
@@ -292,9 +321,9 @@ end
|
|
|
292
321
|
|
|
293
322
|
--- One `get_async` on `key`, no write. Never registered in the store: `close`
|
|
294
323
|
--- does not know about it and hooks do not fire for it.
|
|
295
|
-
function store.peek(self: Store, key: string): PeekProfile
|
|
324
|
+
function store.peek(self: Store, key: string): Result<PeekProfile>
|
|
296
325
|
if self.closed then
|
|
297
|
-
|
|
326
|
+
return result.err(errors.store_closed(self))
|
|
298
327
|
end
|
|
299
328
|
|
|
300
329
|
return peek_profile.create {
|
|
@@ -317,9 +346,9 @@ function store.transaction(
|
|
|
317
346
|
profiles: Array<ProfileBase>,
|
|
318
347
|
transform: (data: Array<any>) -> Array<any> | false,
|
|
319
348
|
config: types.TransactionConfig?
|
|
320
|
-
): boolean
|
|
349
|
+
): Result<boolean>
|
|
321
350
|
if self.closed then
|
|
322
|
-
|
|
351
|
+
return result.err(errors.store_closed(self))
|
|
323
352
|
end
|
|
324
353
|
|
|
325
354
|
local resolved = tx.resolve_config(config, {
|
|
@@ -335,11 +364,11 @@ function store.transaction(
|
|
|
335
364
|
datas[index] = ctx:get(participant)
|
|
336
365
|
end
|
|
337
366
|
|
|
338
|
-
local
|
|
339
|
-
if
|
|
367
|
+
local transformed = transform(datas)
|
|
368
|
+
if transformed == false then
|
|
340
369
|
return false
|
|
341
370
|
end
|
|
342
|
-
local new_datas =
|
|
371
|
+
local new_datas = transformed :: Array<any>
|
|
343
372
|
|
|
344
373
|
for index, participant in profiles do
|
|
345
374
|
local new = new_datas[index]
|
|
@@ -353,7 +382,9 @@ function store.transaction(
|
|
|
353
382
|
end
|
|
354
383
|
|
|
355
384
|
--- Unloads every profile (locked and lockless) concurrently and waits for all
|
|
356
|
-
--- of them
|
|
385
|
+
--- of them, and for any load still in flight (which fails with `store_closed`
|
|
386
|
+
--- and releases its lock). Loads and transactions are rejected from the first
|
|
387
|
+
--- line on.
|
|
357
388
|
function store.close(self: Store)
|
|
358
389
|
if self.closed then
|
|
359
390
|
return
|
|
@@ -379,12 +410,29 @@ function store.close(self: Store)
|
|
|
379
410
|
end)
|
|
380
411
|
end
|
|
381
412
|
|
|
413
|
+
--- A load still in flight: `load` sees `closed` when it finishes and gives
|
|
414
|
+
--- the lock back itself; wait for that like for an unload.
|
|
415
|
+
local function wait_load(key: string)
|
|
416
|
+
remaining += 1
|
|
417
|
+
scheduler.spawn(function()
|
|
418
|
+
insert_loaded_lock(self, key)
|
|
419
|
+
coroutine.yield()
|
|
420
|
+
remaining -= 1
|
|
421
|
+
if remaining == 0 and waiting then
|
|
422
|
+
scheduler.spawn(thread)
|
|
423
|
+
end
|
|
424
|
+
end)
|
|
425
|
+
end
|
|
426
|
+
|
|
382
427
|
for _, profile in self.profiles do
|
|
383
428
|
unload(profile)
|
|
384
429
|
end
|
|
385
430
|
for _, profile in self.lockless_profiles do
|
|
386
431
|
unload(profile)
|
|
387
432
|
end
|
|
433
|
+
for key in self._loading do
|
|
434
|
+
wait_load(key)
|
|
435
|
+
end
|
|
388
436
|
|
|
389
437
|
if remaining > 0 then
|
|
390
438
|
waiting = true
|
package/src/tx/handle.luau
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
foreign `pending` entries.
|
|
8
8
|
]]
|
|
9
9
|
|
|
10
|
+
local errors = require "../errors"
|
|
11
|
+
local result = require "../result"
|
|
10
12
|
local types = require "../types"
|
|
11
13
|
local utils = require "../utils"
|
|
12
14
|
local roblox_scheduler = require "../schedulers/roblox"
|
|
@@ -25,6 +27,7 @@ type ResolvedTransactionConfig = types.ResolvedTransactionConfig
|
|
|
25
27
|
|
|
26
28
|
type TxOutcome = types.TxOutcome
|
|
27
29
|
type TxPatch = types.TxPatch
|
|
30
|
+
type Error = types.Error
|
|
28
31
|
export type TxContext = types.TxContext
|
|
29
32
|
|
|
30
33
|
local tx_handle = {}
|
|
@@ -50,19 +53,17 @@ local function resolve_marker_in(
|
|
|
50
53
|
id: string,
|
|
51
54
|
fallback: TxOutcome
|
|
52
55
|
): TxOutcome
|
|
53
|
-
local
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return fallback
|
|
59
|
-
end)
|
|
56
|
+
local outcome = utils.update_async(config, datastore, id, function(current: any?)
|
|
57
|
+
if current == "committed" or current == "aborted" then
|
|
58
|
+
return current
|
|
59
|
+
end
|
|
60
|
+
return fallback
|
|
60
61
|
end)
|
|
61
62
|
|
|
62
|
-
if
|
|
63
|
-
error(
|
|
63
|
+
if outcome ~= "committed" and outcome ~= "aborted" then
|
|
64
|
+
error(errors.tx_marker_invalid(id, outcome), 0)
|
|
64
65
|
end
|
|
65
|
-
return
|
|
66
|
+
return outcome :: TxOutcome
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
--- Reads the marker for `id`, writing `fallback` if it has not been decided yet.
|
|
@@ -154,42 +155,47 @@ local function prepare_sort(a: TxPatch, b: TxPatch): boolean
|
|
|
154
155
|
return sort_key(a.profile) < sort_key(b.profile)
|
|
155
156
|
end
|
|
156
157
|
|
|
157
|
-
--- Locks every profile mutex in order
|
|
158
|
-
---
|
|
159
|
-
---
|
|
160
|
-
local function
|
|
158
|
+
--- Locks every profile mutex in order, appending each one taken to `locked`
|
|
159
|
+
--- so a rejection midway (a mutex closed while waiting) leaves an exact list
|
|
160
|
+
--- for `unlock_all`.
|
|
161
|
+
local function lock_all(profiles: Array<ProfileBase>, locked: Array<ProfileBase>)
|
|
161
162
|
for _, participant in profiles do
|
|
162
163
|
participant._mutex:lock()
|
|
164
|
+
table.insert(locked, participant)
|
|
163
165
|
end
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
--- Releases the mutexes in reverse and, for a lockless profile still holding
|
|
169
|
+
--- its storage transaction lock (the transaction ended before writing
|
|
170
|
+
--- `pending`), gives the key back.
|
|
171
|
+
local function unlock_all(locked: Array<ProfileBase>)
|
|
172
|
+
for index = #locked, 1, -1 do
|
|
173
|
+
local participant = locked[index]
|
|
174
|
+
local lockless = participant :: LocklessProfile
|
|
175
|
+
if participant.kind == "lockless" and lockless._tx_lock then
|
|
176
|
+
local ok, err = utils.try(function()
|
|
177
|
+
lockless:_write_queued {
|
|
178
|
+
old_tx_lock = lockless._tx_lock,
|
|
179
|
+
}
|
|
180
|
+
end)
|
|
181
|
+
if not ok then
|
|
182
|
+
participant.config._hook.warn(
|
|
183
|
+
`datastore: failed to release tx lock on "{participant.key}" (it will expire): {tostring(err)}`
|
|
184
|
+
)
|
|
185
|
+
lockless._tx_lock = nil
|
|
180
186
|
end
|
|
181
|
-
participant._mutex:unlock()
|
|
182
187
|
end
|
|
188
|
+
participant._mutex:unlock()
|
|
183
189
|
end
|
|
184
190
|
end
|
|
185
191
|
|
|
186
192
|
local function assert_open_profiles(profiles: Array<ProfileBase>)
|
|
187
193
|
for _, participant in profiles do
|
|
188
194
|
if not participant.open then
|
|
189
|
-
error(
|
|
195
|
+
error(errors.closed(participant), 0)
|
|
190
196
|
end
|
|
191
197
|
if participant.kind == "locked" and not (participant :: Profile).is_locked then
|
|
192
|
-
error(
|
|
198
|
+
error(errors.released(participant), 0)
|
|
193
199
|
end
|
|
194
200
|
end
|
|
195
201
|
end
|
|
@@ -230,7 +236,10 @@ local function locked_patch(participant: Profile, new_data: any): TxPatch
|
|
|
230
236
|
patch.prepared = false
|
|
231
237
|
|
|
232
238
|
function patch.prepare(pending)
|
|
233
|
-
|
|
239
|
+
local written, lock = participant:_write_record { release = false, pending = pending }
|
|
240
|
+
if not written then
|
|
241
|
+
error(errors.lock_lost(participant, lock), 0)
|
|
242
|
+
end
|
|
234
243
|
end
|
|
235
244
|
function patch.commit(operation)
|
|
236
245
|
table.insert(participant._cleanup_txs, operation)
|
|
@@ -256,13 +265,13 @@ local function lockless_patch(participant: LocklessProfile, new_data: any): TxPa
|
|
|
256
265
|
patch.prepared = false
|
|
257
266
|
|
|
258
267
|
function patch.prepare(pending)
|
|
259
|
-
-- releases the transaction lock taken in `adquire_lockless_lock`;
|
|
260
|
-
-- (nothing written) if that lock is gone or someone
|
|
268
|
+
-- releases the transaction lock taken in `adquire_lockless_lock`; throws
|
|
269
|
+
-- `tx_lock_lost` (nothing written) if that lock is gone or someone
|
|
270
|
+
-- else's by now
|
|
261
271
|
participant:_write_queued {
|
|
262
272
|
pending = pending,
|
|
263
273
|
old_tx_lock = participant._tx_lock,
|
|
264
274
|
}
|
|
265
|
-
return true
|
|
266
275
|
end
|
|
267
276
|
function patch.commit(operation)
|
|
268
277
|
participant._known_outcomes[operation.id] = "committed"
|
|
@@ -287,13 +296,16 @@ local function make_patch(participant: ProfileBase, new_data: any): TxPatch
|
|
|
287
296
|
end
|
|
288
297
|
|
|
289
298
|
--- Runs `process(ctx)` over the given profiles (any store, locked or lockless)
|
|
290
|
-
--- and commits every `ctx:set` atomically.
|
|
291
|
-
---
|
|
299
|
+
--- and commits every `ctx:set` atomically. Ok(false) when `process` returns
|
|
300
|
+
--- false, Ok(true) when committed. Fails with `tx_aborted` (its `cause` says
|
|
301
|
+
--- why phase 1 gave up), `profile_closed`, `released`, or what flushing a
|
|
302
|
+
--- lockless participant raised (`lockless_locked`, `outdated`, `roblox`, ...).
|
|
303
|
+
--- Argument misuse throws; an error thrown by `process` propagates as is.
|
|
292
304
|
function tx_handle.run(
|
|
293
305
|
config: ResolvedTransactionConfig,
|
|
294
306
|
profiles: Array<ProfileBase>,
|
|
295
307
|
process: (ctx: TxContext) -> boolean?
|
|
296
|
-
): boolean
|
|
308
|
+
): types.Result<boolean>
|
|
297
309
|
local seen: { [ProfileBase]: boolean } = {}
|
|
298
310
|
local profile_list: Array<ProfileBase> = {}
|
|
299
311
|
|
|
@@ -301,18 +313,18 @@ function tx_handle.run(
|
|
|
301
313
|
if not is_profile(profile) then
|
|
302
314
|
error "datastore: transaction expects profiles"
|
|
303
315
|
end
|
|
304
|
-
if not profile.open then
|
|
305
|
-
error "datastore: all profiles in a transaction must be open"
|
|
306
|
-
end
|
|
307
|
-
if profile.kind == "locked" and not (profile :: Profile).is_locked then
|
|
308
|
-
error(`datastore: transaction failed, profile "{profile.key}" does not hold its session lock`)
|
|
309
|
-
end
|
|
310
316
|
if seen[profile] then
|
|
311
317
|
error "datastore: a profile was provided twice in a transaction"
|
|
312
318
|
end
|
|
313
319
|
if profile.config._hook ~= config._hook then
|
|
314
320
|
error "datastore: all profiles in a transaction must share the transaction's hook"
|
|
315
321
|
end
|
|
322
|
+
if not profile.open then
|
|
323
|
+
return result.err(errors.closed(profile))
|
|
324
|
+
end
|
|
325
|
+
if profile.kind == "locked" and not (profile :: Profile).is_locked then
|
|
326
|
+
return result.err(errors.released(profile))
|
|
327
|
+
end
|
|
316
328
|
|
|
317
329
|
seen[profile] = true
|
|
318
330
|
table.insert(profile_list, profile)
|
|
@@ -323,9 +335,12 @@ function tx_handle.run(
|
|
|
323
335
|
end
|
|
324
336
|
profile_list = utils.sorted(profile_list, profile_sort)
|
|
325
337
|
|
|
326
|
-
local
|
|
338
|
+
local locked: Array<ProfileBase> = {}
|
|
327
339
|
|
|
328
|
-
local ok,
|
|
340
|
+
local ok, value = utils.try(function()
|
|
341
|
+
-- inside the protected region: a mutex closed while waiting rejects the
|
|
342
|
+
-- lock, and the ones already taken must still be given back
|
|
343
|
+
lock_all(profile_list, locked)
|
|
329
344
|
assert_open_profiles(profile_list)
|
|
330
345
|
-- kind of an oxymoron
|
|
331
346
|
adquire_lockless_lock(config, profile_list)
|
|
@@ -385,7 +400,10 @@ function tx_handle.run(
|
|
|
385
400
|
-- Phase 1: every changed participant persists its post-tx data as `pending`.
|
|
386
401
|
local order = utils.sorted(patches, prepare_sort)
|
|
387
402
|
local desired: TxOutcome = "committed"
|
|
388
|
-
|
|
403
|
+
-- why phase 1 gave up: a library error (reported as the abort's cause)
|
|
404
|
+
-- or a raw one (an internal invariant; rethrown as is after settling)
|
|
405
|
+
local cause: Error? = nil
|
|
406
|
+
local raw: any = nil
|
|
389
407
|
local expires = config._scheduler.now() + config.tx_ttl
|
|
390
408
|
local store_name = config._datastore.name
|
|
391
409
|
|
|
@@ -396,13 +414,16 @@ function tx_handle.run(
|
|
|
396
414
|
expires = expires,
|
|
397
415
|
data = patch.new_data,
|
|
398
416
|
}
|
|
399
|
-
local write_ok,
|
|
400
|
-
if write_ok
|
|
417
|
+
local write_ok, err = utils.try(patch.prepare, pending)
|
|
418
|
+
if write_ok then
|
|
401
419
|
patch.prepared = true
|
|
402
420
|
else
|
|
403
421
|
desired = "aborted"
|
|
404
|
-
|
|
405
|
-
|
|
422
|
+
if errors.is(err) then
|
|
423
|
+
cause = err
|
|
424
|
+
else
|
|
425
|
+
raw = err
|
|
426
|
+
end
|
|
406
427
|
break
|
|
407
428
|
end
|
|
408
429
|
end
|
|
@@ -414,14 +435,16 @@ function tx_handle.run(
|
|
|
414
435
|
if outcome == "committed" then
|
|
415
436
|
return true
|
|
416
437
|
end
|
|
417
|
-
|
|
438
|
+
if raw ~= nil then
|
|
439
|
+
error(raw, 0)
|
|
440
|
+
end
|
|
441
|
+
-- no cause: every prepare went through but the marker was already
|
|
442
|
+
-- resolved as aborted by another server (the pending's grace expired)
|
|
443
|
+
error(errors.tx_aborted(id, cause), 0)
|
|
418
444
|
end)
|
|
419
445
|
|
|
420
|
-
unlock_all()
|
|
421
|
-
|
|
422
|
-
error(result)
|
|
423
|
-
end
|
|
424
|
-
return result
|
|
446
|
+
unlock_all(locked)
|
|
447
|
+
return result.of(ok, value)
|
|
425
448
|
end
|
|
426
449
|
|
|
427
450
|
return tx_handle
|
package/src/tx/operation.luau
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
local errors = require "../errors"
|
|
1
2
|
local types = require "../types"
|
|
2
3
|
|
|
3
4
|
export type TxOperation = types.TxOperation
|
|
@@ -23,14 +24,16 @@ local function resolve_marker(self: TxOperation): TxOutcome
|
|
|
23
24
|
end)
|
|
24
25
|
|
|
25
26
|
if result ~= "committed" and result ~= "aborted" then
|
|
26
|
-
error(
|
|
27
|
+
error(errors.tx_marker_invalid(id, result), 0)
|
|
27
28
|
end
|
|
28
29
|
return result :: TxOutcome
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
--- Resolves the marker (blocking, retrying with backoff until the storage
|
|
32
|
-
--- answers), then applies the outcome to every patch
|
|
33
|
-
---
|
|
33
|
+
--- answers with a valid outcome), then applies the outcome to every patch
|
|
34
|
+
--- exactly once. Never fails: every participant mutex is held meanwhile, so
|
|
35
|
+
--- giving up would leave them without an outcome. Safe to call from several
|
|
36
|
+
--- coroutines: later callers wait for the first resolution.
|
|
34
37
|
function tx_operation.settle(self: TxOperation): TxOutcome
|
|
35
38
|
local scheduler = self.config._scheduler
|
|
36
39
|
|