@rbxts/dataforge 0.1.0 → 0.4.1
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/LICENSE +21 -0
- package/index.d.ts +318 -32
- package/package.json +7 -2
- package/src/errors.luau +211 -0
- package/src/hooks/memory.luau +1 -12
- package/src/hooks/roblox.luau +10 -1
- package/src/init.luau +20 -2
- package/src/mutex.luau +5 -5
- package/src/profile/common.luau +61 -12
- package/src/profile/lockless.luau +122 -51
- package/src/profile/peek.luau +30 -16
- package/src/profile/profile.luau +246 -116
- package/src/result.luau +61 -0
- package/src/store.luau +129 -44
- package/src/tx/handle.luau +82 -59
- package/src/tx/operation.luau +6 -3
- package/src/types.luau +236 -17
- package/src/utils.luau +37 -3
package/src/profile/profile.luau
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
local errors = require "../errors"
|
|
1
2
|
local mutex = require "../mutex"
|
|
3
|
+
local result = require "../result"
|
|
2
4
|
local types = require "../types"
|
|
3
5
|
local utils = require "../utils"
|
|
4
6
|
local common = require "./common"
|
|
@@ -13,22 +15,57 @@ export type LoadOptions = types.LoadOptions
|
|
|
13
15
|
local profile = {}
|
|
14
16
|
profile.__index = profile
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
--- One counter per process: a lock id must be unique per acquisition, not per
|
|
19
|
+
--- server, so two sessions of the same key on this server can tell their locks
|
|
20
|
+
--- apart. `#` keeps them out of the transaction id namespace (`{job_id}-{n}`),
|
|
21
|
+
--- which lockless writes compare by id.
|
|
22
|
+
local lock_counter = utils.counter()
|
|
23
|
+
|
|
24
|
+
--- Takes the session lock on `key`, waiting out (or stealing by expiry) any
|
|
25
|
+
--- holder. Returns the locked record and the id this acquisition stamped on it.
|
|
26
|
+
local function adquire_lock(load_options: LoadOptions): (types.DataRecord, string)
|
|
17
27
|
local config = load_options.config
|
|
18
28
|
local user_ids = load_options.user_ids
|
|
19
29
|
local datastore = load_options._datastore
|
|
20
|
-
local job_id = config._hook.job_id()
|
|
21
30
|
local key = load_options.key
|
|
31
|
+
-- minted once per acquisition, before the loop: waiting may write the lock
|
|
32
|
+
-- several times and every attempt must stamp the same id
|
|
33
|
+
local lock_id = `{config._hook.job_id()}#{lock_counter()}`
|
|
22
34
|
|
|
23
35
|
local scheduler = config._scheduler
|
|
24
36
|
|
|
25
37
|
local deadline = scheduler.now() + config.load_timeout
|
|
38
|
+
-- the last lock seen on the record, reported when the load times out
|
|
39
|
+
local last_lock: types.Lock? = nil
|
|
40
|
+
-- set by the transform: the record lists more migrations than this server
|
|
41
|
+
-- declares, so the lock must not be taken (the write is cancelled)
|
|
42
|
+
local outdated_migrations: { string }? = nil
|
|
43
|
+
-- set by the transform: the error the store's predicate rejected the record
|
|
44
|
+
-- with (or the one the predicate itself threw); the lock is not taken
|
|
45
|
+
local predicate_error: any = nil
|
|
26
46
|
|
|
27
47
|
while true do
|
|
28
48
|
local now = scheduler.now()
|
|
29
|
-
local lock_applied = utils.
|
|
30
|
-
|
|
31
|
-
|
|
49
|
+
local lock_applied = utils.update_async(
|
|
50
|
+
config,
|
|
51
|
+
datastore,
|
|
52
|
+
key,
|
|
53
|
+
function(old: types.DataRecord?): types.DataRecord?
|
|
54
|
+
outdated_migrations = nil
|
|
55
|
+
predicate_error = nil
|
|
56
|
+
if common.is_outdated(old, config) then
|
|
57
|
+
outdated_migrations = (old :: types.DataRecord).migrations
|
|
58
|
+
return nil
|
|
59
|
+
end
|
|
60
|
+
-- before the lock check on purpose: a key this server must not
|
|
61
|
+
-- own is rejected right away instead of waiting out its holder
|
|
62
|
+
local stored = if old then old.data else nil
|
|
63
|
+
local passed, failure = common.run_predicate(config, key, stored)
|
|
64
|
+
if not passed then
|
|
65
|
+
predicate_error = failure or errors.predicate_failed(key, "load", stored)
|
|
66
|
+
return nil
|
|
67
|
+
end
|
|
68
|
+
if common.is_locked_for(old :: types.DataRecord, lock_id, now) then
|
|
32
69
|
return nil
|
|
33
70
|
end
|
|
34
71
|
local record: types.DataRecord = old or {} :: types.DataRecord
|
|
@@ -38,28 +75,35 @@ local function adquire_lock(load_options: LoadOptions): types.DataRecord
|
|
|
38
75
|
pending = record.pending,
|
|
39
76
|
migrations = record.migrations or {},
|
|
40
77
|
lock = {
|
|
41
|
-
id =
|
|
78
|
+
id = lock_id,
|
|
42
79
|
expires = now + config.lock_ttl,
|
|
43
80
|
},
|
|
44
81
|
}
|
|
45
|
-
end,
|
|
46
|
-
|
|
82
|
+
end,
|
|
83
|
+
user_ids
|
|
84
|
+
) :: types.DataRecord
|
|
47
85
|
|
|
48
|
-
if
|
|
49
|
-
|
|
86
|
+
if outdated_migrations ~= nil then
|
|
87
|
+
error(errors.outdated(key, outdated_migrations, common.all_migration_names(config.migrations)), 0)
|
|
88
|
+
end
|
|
89
|
+
if predicate_error ~= nil then
|
|
90
|
+
error(predicate_error, 0)
|
|
50
91
|
end
|
|
92
|
+
if common.is_lock_owned(lock_applied, lock_id) then
|
|
93
|
+
return lock_applied, lock_id
|
|
94
|
+
end
|
|
95
|
+
last_lock = if lock_applied then lock_applied.lock else nil
|
|
51
96
|
|
|
52
97
|
while true do
|
|
53
98
|
if scheduler.now() >= deadline then
|
|
54
|
-
error(
|
|
99
|
+
error(errors.timeout(key, last_lock), 0)
|
|
55
100
|
end
|
|
56
101
|
scheduler.wait(config.load_poll)
|
|
57
102
|
|
|
58
|
-
local current = utils.
|
|
59
|
-
|
|
60
|
-
end) :: types.DataRecord?
|
|
103
|
+
local current = utils.get_async(config, datastore, key) :: types.DataRecord?
|
|
104
|
+
last_lock = if current then current.lock else nil
|
|
61
105
|
|
|
62
|
-
if not common.is_locked_for(current,
|
|
106
|
+
if not common.is_locked_for(current, lock_id, scheduler.now()) then
|
|
63
107
|
break
|
|
64
108
|
end
|
|
65
109
|
end
|
|
@@ -85,9 +129,11 @@ local function run_autosave_loop(self: Profile)
|
|
|
85
129
|
|
|
86
130
|
-- a failed write must not kill the loop: the next tick retries, and
|
|
87
131
|
-- `write_data_record` gives the lock up after a full ttl of failures
|
|
88
|
-
local ok,
|
|
132
|
+
local ok, res = utils.try(self.save, self)
|
|
89
133
|
if not ok then
|
|
90
|
-
config._hook.warn(`datastore: autosave failed for "{self.key}": {tostring(
|
|
134
|
+
config._hook.warn(`datastore: autosave failed for "{self.key}": {tostring(res)}`)
|
|
135
|
+
elseif not res.success then
|
|
136
|
+
config._hook.warn(`datastore: autosave failed for "{self.key}": {tostring(res.error)}`)
|
|
91
137
|
end
|
|
92
138
|
end
|
|
93
139
|
end)
|
|
@@ -109,6 +155,18 @@ local function set_data(self: Profile, data: any)
|
|
|
109
155
|
fire_hooks(self, self._hooks.change, new, old)
|
|
110
156
|
end
|
|
111
157
|
|
|
158
|
+
--- Lets the store drop the key as soon as the profile stops being usable,
|
|
159
|
+
--- without waiting for the final write to land. Idempotent: a lost lock
|
|
160
|
+
--- followed by an unload must only detach once.
|
|
161
|
+
local function detach(self: Profile)
|
|
162
|
+
local on_detach = self._on_detach
|
|
163
|
+
if on_detach == nil or self._detached then
|
|
164
|
+
return
|
|
165
|
+
end
|
|
166
|
+
self._detached = true
|
|
167
|
+
on_detach()
|
|
168
|
+
end
|
|
169
|
+
|
|
112
170
|
--- Another server took the key: the profile closes without a release write.
|
|
113
171
|
--- Runs inside a write (mutex held by the caller), so it only closes the mutex:
|
|
114
172
|
--- the holder finishes, everything waiting on it is rejected.
|
|
@@ -117,7 +175,8 @@ local function process_lock_lost(self: Profile)
|
|
|
117
175
|
return
|
|
118
176
|
end
|
|
119
177
|
self.open = false
|
|
120
|
-
|
|
178
|
+
detach(self)
|
|
179
|
+
self._mutex:close(errors.profile_closed(self, "lock_lost"))
|
|
121
180
|
fire_hooks(self, self._hooks.lock_lost)
|
|
122
181
|
fire_hooks(self, self._hooks.closed)
|
|
123
182
|
end
|
|
@@ -127,20 +186,40 @@ type WriteOptions = {
|
|
|
127
186
|
pending: types.Pending?,
|
|
128
187
|
}
|
|
129
188
|
|
|
130
|
-
|
|
189
|
+
--- Writes the data (and `pending`, if any) under our session lock, refreshing
|
|
190
|
+
--- or releasing it. Returns true when written; false and the lock now on the
|
|
191
|
+
--- record (nil when none) when the lock is not ours any more, in which case
|
|
192
|
+
--- the profile is closed. Throws a `roblox` error when storage fails, or
|
|
193
|
+
--- `predicate_failed` when the store's predicate rejected the stored data
|
|
194
|
+
--- (nothing is written and the lock is kept).
|
|
195
|
+
local function write_data_record(self: Profile, options: WriteOptions): (boolean, types.Lock?)
|
|
131
196
|
local config = self.config
|
|
132
197
|
local scheduler = config._scheduler
|
|
133
198
|
local datastore = self._datastore
|
|
134
199
|
local now = scheduler.now()
|
|
135
|
-
|
|
200
|
+
-- this acquisition's id, not the job id: another session of this key on this
|
|
201
|
+
-- server holds a different lock and must not be written over
|
|
202
|
+
local lock_id = self.lock_id
|
|
136
203
|
|
|
137
204
|
-- set by the transform: whether the write went through (the transform may
|
|
138
|
-
-- run more than once when the storage retries it)
|
|
205
|
+
-- run more than once when the storage retries it), and the lock it found
|
|
139
206
|
local applied = false
|
|
207
|
+
local seen_lock: types.Lock? = nil
|
|
208
|
+
-- set by the transform: the store's predicate rejected the stored data (or
|
|
209
|
+
-- threw); the write is cancelled without the lock being lost
|
|
210
|
+
local predicate_error: any = nil
|
|
140
211
|
|
|
141
212
|
local function transform(old: any?): DataRecord?
|
|
142
213
|
applied = false
|
|
143
|
-
|
|
214
|
+
predicate_error = nil
|
|
215
|
+
seen_lock = if old then old.lock else nil
|
|
216
|
+
if not common.is_lock_owned(old, lock_id) then
|
|
217
|
+
return nil
|
|
218
|
+
end
|
|
219
|
+
local stored = if old then old.data else nil
|
|
220
|
+
local passed, failure = common.run_predicate(config, self.key, stored)
|
|
221
|
+
if not passed then
|
|
222
|
+
predicate_error = failure or errors.predicate_failed(self.key, "save", stored, self)
|
|
144
223
|
return nil
|
|
145
224
|
end
|
|
146
225
|
applied = true
|
|
@@ -148,7 +227,9 @@ local function write_data_record(self: Profile, options: WriteOptions): boolean
|
|
|
148
227
|
local new_lock: types.Lock? = nil
|
|
149
228
|
if not options.release then
|
|
150
229
|
new_lock = {
|
|
151
|
-
id
|
|
230
|
+
-- the same id every time: a fresh one would look like a new lock
|
|
231
|
+
-- to everyone else
|
|
232
|
+
id = lock_id,
|
|
152
233
|
expires = now + config.lock_ttl,
|
|
153
234
|
tx = nil,
|
|
154
235
|
} :: types.Lock
|
|
@@ -163,9 +244,7 @@ local function write_data_record(self: Profile, options: WriteOptions): boolean
|
|
|
163
244
|
end
|
|
164
245
|
|
|
165
246
|
local success, err = pcall(function()
|
|
166
|
-
utils.
|
|
167
|
-
return datastore.update_async(self.key, transform, self.user_ids)
|
|
168
|
-
end)
|
|
247
|
+
utils.update_async(config, datastore, self.key, transform, self.user_ids)
|
|
169
248
|
end)
|
|
170
249
|
|
|
171
250
|
if not success then
|
|
@@ -177,10 +256,16 @@ local function write_data_record(self: Profile, options: WriteOptions): boolean
|
|
|
177
256
|
error(err, 0)
|
|
178
257
|
end
|
|
179
258
|
|
|
259
|
+
if predicate_error ~= nil then
|
|
260
|
+
-- the predicate cancelled the write, not a lost lock: the profile keeps
|
|
261
|
+
-- its lock and stays open, so the next save may go through again
|
|
262
|
+
error(predicate_error, 0)
|
|
263
|
+
end
|
|
264
|
+
|
|
180
265
|
if not applied then
|
|
181
266
|
-- the transform bailed out: the lock is not ours any more
|
|
182
267
|
process_lock_lost(self)
|
|
183
|
-
return false
|
|
268
|
+
return false, seen_lock
|
|
184
269
|
end
|
|
185
270
|
|
|
186
271
|
self.last_write = now
|
|
@@ -197,31 +282,33 @@ local function write_data_record(self: Profile, options: WriteOptions): boolean
|
|
|
197
282
|
return true
|
|
198
283
|
end
|
|
199
284
|
|
|
200
|
-
--- Gives the lock taken by `adquire_lock` back, leaving the record
|
|
201
|
-
--- untouched. Used when the load fails after locking; a failure here
|
|
202
|
-
--- warned about (the lock expires on its own).
|
|
203
|
-
local
|
|
285
|
+
--- Gives the lock `lock_id` (taken by `adquire_lock`) back, leaving the record
|
|
286
|
+
--- otherwise untouched. Used when the load fails after locking; a failure here
|
|
287
|
+
--- is only warned about (the lock expires on its own). Takes the acquisition id
|
|
288
|
+
--- so a failed load cannot clear a lock another local acquisition just took.
|
|
289
|
+
local function release_lock(load_options: LoadOptions, lock_id: string)
|
|
204
290
|
local config = load_options.config
|
|
205
|
-
local job_id = config._hook.job_id()
|
|
206
291
|
|
|
207
292
|
local ok, err = utils.try(function()
|
|
208
|
-
utils.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
end, load_options.user_ids)
|
|
221
|
-
end)
|
|
293
|
+
utils.update_async(config, load_options._datastore, load_options.key, function(old: DataRecord?): DataRecord?
|
|
294
|
+
if not common.is_lock_owned(old, lock_id) then
|
|
295
|
+
return nil
|
|
296
|
+
end
|
|
297
|
+
local record = old :: DataRecord
|
|
298
|
+
return {
|
|
299
|
+
data = record.data,
|
|
300
|
+
pending = record.pending,
|
|
301
|
+
migrations = record.migrations,
|
|
302
|
+
lock = nil,
|
|
303
|
+
}
|
|
304
|
+
end, load_options.user_ids)
|
|
222
305
|
end)
|
|
223
306
|
if not ok then
|
|
224
|
-
config._hook.warn(
|
|
307
|
+
config._hook.warn(
|
|
308
|
+
`datastore: could not release the lock of "{load_options.key}" after a failed load (it will expire): {tostring(
|
|
309
|
+
err
|
|
310
|
+
)}`
|
|
311
|
+
)
|
|
225
312
|
end
|
|
226
313
|
end
|
|
227
314
|
|
|
@@ -253,15 +340,15 @@ local function resolve_loaded(load_options: LoadOptions, record: DataRecord): (a
|
|
|
253
340
|
end
|
|
254
341
|
dirty = true
|
|
255
342
|
else
|
|
256
|
-
local
|
|
257
|
-
if not
|
|
258
|
-
error(
|
|
343
|
+
local migrated = apply_migrations(load_options.key, data, record.migrations or {}, migrations)
|
|
344
|
+
if not migrated.success then
|
|
345
|
+
error(migrated.err, 0)
|
|
259
346
|
end
|
|
260
347
|
|
|
261
|
-
data =
|
|
262
|
-
new_migrations =
|
|
348
|
+
data = migrated.result
|
|
349
|
+
new_migrations = migrated.new_migrations
|
|
263
350
|
|
|
264
|
-
if
|
|
351
|
+
if migrated.applied then
|
|
265
352
|
dirty = true
|
|
266
353
|
end
|
|
267
354
|
end
|
|
@@ -271,19 +358,21 @@ end
|
|
|
271
358
|
|
|
272
359
|
--- Takes the session lock and resolves the locked record into data. If the
|
|
273
360
|
--- resolution fails the lock is given back before rethrowing.
|
|
274
|
-
local function adquire_and_resolve(load_options: LoadOptions): (any, { string }, boolean)
|
|
275
|
-
local record = adquire_lock(load_options)
|
|
361
|
+
local function adquire_and_resolve(load_options: LoadOptions): (any, { string }, boolean, string)
|
|
362
|
+
local record, lock_id = adquire_lock(load_options)
|
|
276
363
|
local config = load_options.config
|
|
277
364
|
|
|
278
365
|
-- from here on the key is locked: anything failing must give it back
|
|
279
366
|
local ok, data, new_migrations, dirty = pcall(resolve_loaded, load_options, record)
|
|
280
367
|
if not ok then
|
|
281
368
|
local err = data
|
|
282
|
-
config._hook.warn(
|
|
283
|
-
|
|
369
|
+
config._hook.warn(
|
|
370
|
+
`datastore: load of "{load_options.key}" failed after taking its lock, releasing it: {tostring(err)}`
|
|
371
|
+
)
|
|
372
|
+
release_lock(load_options, lock_id)
|
|
284
373
|
error(err, 0)
|
|
285
374
|
end
|
|
286
|
-
return data, new_migrations, dirty
|
|
375
|
+
return data, new_migrations, dirty, lock_id
|
|
287
376
|
end
|
|
288
377
|
|
|
289
378
|
local function load_options_of(self: Profile): LoadOptions
|
|
@@ -297,12 +386,12 @@ end
|
|
|
297
386
|
|
|
298
387
|
local function assert_locked(self: Profile)
|
|
299
388
|
if not self.is_locked then
|
|
300
|
-
error(
|
|
389
|
+
error(errors.released(self), 0)
|
|
301
390
|
end
|
|
302
391
|
end
|
|
303
392
|
|
|
304
393
|
function profile.load(load_options: LoadOptions): Profile
|
|
305
|
-
local data, new_migrations, dirty = adquire_and_resolve(load_options)
|
|
394
|
+
local data, new_migrations, dirty, lock_id = adquire_and_resolve(load_options)
|
|
306
395
|
local config = load_options.config
|
|
307
396
|
|
|
308
397
|
local self: Profile = {} :: any
|
|
@@ -312,6 +401,8 @@ function profile.load(load_options: LoadOptions): Profile
|
|
|
312
401
|
self._datastore = load_options._datastore
|
|
313
402
|
self.open = true
|
|
314
403
|
self.is_locked = true
|
|
404
|
+
self.lock_id = lock_id
|
|
405
|
+
self._detached = false
|
|
315
406
|
|
|
316
407
|
self.config = load_options.config
|
|
317
408
|
self.migrations = new_migrations
|
|
@@ -343,22 +434,26 @@ function profile.get_data(self: Profile)
|
|
|
343
434
|
end
|
|
344
435
|
|
|
345
436
|
--- Applies `dispatcher` to the current data. Returning nil or false leaves the
|
|
346
|
-
--- data untouched (
|
|
347
|
-
--- persisted on the next save. Waits for any in-flight save on the
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
-- a release may have run while this call waited on the mutex
|
|
437
|
+
--- data untouched (Ok(false)); anything else becomes the new data and is
|
|
438
|
+
--- persisted on the next save (Ok(true)). Waits for any in-flight save on the
|
|
439
|
+
--- mutex. Fails with `profile_closed` / `released`; an error thrown by the
|
|
440
|
+
--- dispatcher propagates as is.
|
|
441
|
+
function profile.update(self: Profile, dispatcher: (any) -> any): types.Result<boolean>
|
|
442
|
+
return result.capture(function()
|
|
443
|
+
common.assert_open(self)
|
|
354
444
|
assert_locked(self)
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
445
|
+
|
|
446
|
+
return self._mutex:with(function()
|
|
447
|
+
-- a release may have run while this call waited on the mutex
|
|
448
|
+
assert_locked(self)
|
|
449
|
+
local new = dispatcher(self._data)
|
|
450
|
+
if new == nil or new == false then
|
|
451
|
+
return false
|
|
452
|
+
end
|
|
453
|
+
set_data(self, new)
|
|
454
|
+
self._dirty = true
|
|
455
|
+
return true
|
|
456
|
+
end)
|
|
362
457
|
end)
|
|
363
458
|
end
|
|
364
459
|
|
|
@@ -382,61 +477,92 @@ function profile.on_lock_lost(self: Profile, callback: () -> ())
|
|
|
382
477
|
table.insert(self._hooks.lock_lost.hooks, callback)
|
|
383
478
|
end
|
|
384
479
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
--
|
|
480
|
+
--- Writes the data, refreshing the session lock. Fails with `profile_closed`,
|
|
481
|
+
--- `released`, `roblox`, `predicate_failed` (the store's predicate rejected
|
|
482
|
+
--- the stored data; nothing is written and the lock is kept), or `lock_lost`
|
|
483
|
+
--- (another server took the key; the profile is closed).
|
|
484
|
+
function profile.save(self: Profile): types.Result<nil>
|
|
485
|
+
return result.capture(function()
|
|
486
|
+
common.assert_open(self)
|
|
487
|
+
assert_locked(self)
|
|
488
|
+
-- before the mutex so hooks can still run updates that land in this save
|
|
489
|
+
fire_hooks(self, self._hooks.save)
|
|
490
|
+
common.assert_open(self)
|
|
394
491
|
assert_locked(self)
|
|
395
|
-
|
|
492
|
+
self._mutex:with(function()
|
|
493
|
+
-- without the lock the write would be cancelled and wrongly read as lost
|
|
494
|
+
assert_locked(self)
|
|
495
|
+
local written, lock = write_data_record(self, { release = false })
|
|
496
|
+
if not written then
|
|
497
|
+
error(errors.lock_lost(self, lock), 0)
|
|
498
|
+
end
|
|
499
|
+
end)
|
|
500
|
+
return nil
|
|
396
501
|
end)
|
|
397
502
|
end
|
|
398
503
|
|
|
399
504
|
--- Writes the data and gives the session lock back. The profile stays open
|
|
400
|
-
--- (another server may load the key meanwhile); `update` and `save`
|
|
401
|
-
--- until `readquire`. No-op when already released.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
return
|
|
505
|
+
--- (another server may load the key meanwhile); `update` and `save` fail with
|
|
506
|
+
--- `released` until `readquire`. No-op when already released. Fails with
|
|
507
|
+
--- `profile_closed`, `roblox` or `predicate_failed` (the lock stays on the
|
|
508
|
+
--- record, `is_locked` stays true), or `lock_lost` (the profile is closed).
|
|
509
|
+
function profile.release(self: Profile): types.Result<nil>
|
|
510
|
+
return result.capture(function()
|
|
511
|
+
common.assert_open(self)
|
|
512
|
+
if not self.is_locked then
|
|
513
|
+
return nil
|
|
410
514
|
end
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
515
|
+
self._mutex:with(function()
|
|
516
|
+
-- an unload queued behind this call has flipped `open`: its own write
|
|
517
|
+
-- gives the lock back
|
|
518
|
+
if not self.open or not self.is_locked then
|
|
519
|
+
return
|
|
520
|
+
end
|
|
521
|
+
local written, lock = write_data_record(self, { release = true })
|
|
522
|
+
if not written then
|
|
523
|
+
error(errors.lock_lost(self, lock), 0)
|
|
524
|
+
end
|
|
414
525
|
self.is_locked = false
|
|
415
|
-
end
|
|
526
|
+
end)
|
|
527
|
+
return nil
|
|
416
528
|
end)
|
|
417
529
|
end
|
|
418
530
|
|
|
419
531
|
--- Takes the session lock again, waiting for a holder like `load` does, and
|
|
420
532
|
--- adopts the stored record (resolved pending, migrations) as the current
|
|
421
|
-
--- data.
|
|
422
|
-
---
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
return
|
|
427
|
-
end
|
|
428
|
-
self._mutex:with(function()
|
|
533
|
+
--- data. Fails with `timeout` (the profile stays open and released),
|
|
534
|
+
--- `profile_closed`, `roblox`, `outdated`, `predicate_failed`,
|
|
535
|
+
--- `migration_mismatch` or `tx_marker_invalid`. No-op when already locked.
|
|
536
|
+
function profile.readquire(self: Profile): types.Result<nil>
|
|
537
|
+
return result.capture(function()
|
|
429
538
|
common.assert_open(self)
|
|
430
539
|
if self.is_locked then
|
|
431
|
-
return
|
|
540
|
+
return nil
|
|
432
541
|
end
|
|
433
|
-
|
|
542
|
+
self._mutex:with(function()
|
|
543
|
+
common.assert_open(self)
|
|
544
|
+
if self.is_locked then
|
|
545
|
+
return
|
|
546
|
+
end
|
|
547
|
+
local ok, data, new_migrations, dirty, lock_id = pcall(adquire_and_resolve, load_options_of(self))
|
|
548
|
+
if not ok then
|
|
549
|
+
local err: any = data
|
|
550
|
+
if errors.is(err) and err.type == "outdated" then
|
|
551
|
+
err.profile = self
|
|
552
|
+
end
|
|
553
|
+
error(err, 0)
|
|
554
|
+
end
|
|
434
555
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
556
|
+
self.migrations = new_migrations
|
|
557
|
+
-- a fresh acquisition, so a fresh id (the released profile kept the
|
|
558
|
+
-- old one; nothing could be written with it while released)
|
|
559
|
+
self.lock_id = lock_id
|
|
560
|
+
set_data(self, data)
|
|
561
|
+
self._dirty = dirty
|
|
562
|
+
self.last_write = self.config._scheduler.now()
|
|
563
|
+
self.is_locked = true
|
|
564
|
+
end)
|
|
565
|
+
return nil
|
|
440
566
|
end)
|
|
441
567
|
end
|
|
442
568
|
|
|
@@ -451,10 +577,14 @@ function profile.unload(self: Profile)
|
|
|
451
577
|
end
|
|
452
578
|
|
|
453
579
|
self.open = false
|
|
580
|
+
-- before the release write (which may take seconds under throttling): the
|
|
581
|
+
-- key is free to be loaded again, the new load waits on the record lock
|
|
582
|
+
detach(self)
|
|
454
583
|
self._mutex:lock()
|
|
455
|
-
self._mutex:close(
|
|
584
|
+
self._mutex:close(errors.profile_closed(self, "unloaded"))
|
|
456
585
|
local ok, err = utils.try(function()
|
|
457
|
-
-- an already released profile has nothing on the record to give back
|
|
586
|
+
-- an already released profile has nothing on the record to give back;
|
|
587
|
+
-- a lost lock (false) has nothing to give back either
|
|
458
588
|
if self.is_locked then
|
|
459
589
|
write_data_record(self, { release = true })
|
|
460
590
|
end
|
package/src/result.luau
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
|
|
3
|
+
local errors = require "./errors"
|
|
4
|
+
local types = require "./types"
|
|
5
|
+
local utils = require "./utils"
|
|
6
|
+
|
|
7
|
+
--[[
|
|
8
|
+
`Result<T>`: what every fallible library call returns instead of throwing.
|
|
9
|
+
|
|
10
|
+
Internally the library keeps throwing, but it throws `Error` tables (see
|
|
11
|
+
`errors.luau`). `result.capture` at each public entry point turns an
|
|
12
|
+
`Error` into `Err`; anything else (a string thrown by a user callback, an
|
|
13
|
+
internal invariant) is rethrown untouched.
|
|
14
|
+
]]
|
|
15
|
+
|
|
16
|
+
export type Result<T> = types.Result<T>
|
|
17
|
+
export type Error = types.Error
|
|
18
|
+
|
|
19
|
+
local result_mt = {}
|
|
20
|
+
result_mt.__index = result_mt
|
|
21
|
+
|
|
22
|
+
function result_mt.unwrap(self: any): any
|
|
23
|
+
if self.success then
|
|
24
|
+
return self.value
|
|
25
|
+
end
|
|
26
|
+
error(self.error, 0)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
local result = {}
|
|
30
|
+
|
|
31
|
+
function result.ok<T>(value: T): Result<T>
|
|
32
|
+
return setmetatable({ success = true, value = value }, result_mt) :: any
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
function result.err(err: Error): Result<any>
|
|
36
|
+
assert(errors.is(err), "result.err expects an Error table")
|
|
37
|
+
return setmetatable({ success = false, error = err }, result_mt) :: any
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
function result.is(value: any): boolean
|
|
41
|
+
return type(value) == "table" and getmetatable(value) == result_mt
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
--- Turns a `pcall` outcome into a Result: `Ok(value)` on success, `Err` when
|
|
45
|
+
--- the thrown value is an `Error` table, rethrown raw otherwise.
|
|
46
|
+
function result.of<T>(ok: boolean, value: any): Result<T>
|
|
47
|
+
if ok then
|
|
48
|
+
return result.ok(value)
|
|
49
|
+
end
|
|
50
|
+
if errors.is(value) then
|
|
51
|
+
return result.err(value)
|
|
52
|
+
end
|
|
53
|
+
error(value, 0)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
--- `pcall(fn, ...)` folded through `result.of`.
|
|
57
|
+
function result.capture<T>(fn: (...any) -> T, ...: any): Result<T>
|
|
58
|
+
return result.of(utils.try(fn, ...))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
return result
|