Skip to content

Commit 1a41b3d

Browse files
committed
gc2: guard weak writes during weak drain
1 parent a593ac2 commit 1a41b3d

13 files changed

Lines changed: 304 additions & 57 deletions

File tree

etc/threading.lua

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---@meta threading
22

33
---@alias threading.timeout_error "timeout"
4-
---@alias threading.thread_fn fun(...):...
4+
---@alias threading.thread_fn fun(...: any): ...
55
---@alias threading.recv_status true|false|"timeout"
66
---@alias threading.peek_status true|false
77
---@alias threading.send_result true|nil
@@ -159,9 +159,8 @@ function threading_mutex:unlock() end
159159
---@nodiscard
160160
function threading_mutex:__tostring() end
161161

162-
---@generic T
163162
---@class threading.channel<T>: userdata
164-
---@field send fun(self: threading.channel<T>, value: T, timeout?: number): threading.send_result, threading.timeout_error|nil
163+
---@field send fun(self: threading.channel<T>, value: T, timeout?: number): threading.send_result, threading.timeout_error?
165164
---@field recv fun(self: threading.channel<T>, timeout?: number): T|nil, threading.recv_status
166165
---@field peek fun(self: threading.channel<T>): T|nil, threading.peek_status
167166
---@field close fun(self: threading.channel<T>): nil
@@ -171,7 +170,6 @@ local threading_channel = {}
171170
---
172171
---Errors if the channel is closed. If this is a rendezvous channel, success
173172
---means a receiver has taken the value.
174-
---@generic T
175173
---@overload fun(self: threading.channel<T>, value: T): true
176174
---@overload fun(self: threading.channel<T>, value: T, timeout: number): true
177175
---@overload fun(self: threading.channel<T>, value: T, timeout: number): nil, threading.timeout_error
@@ -185,7 +183,6 @@ function threading_channel:send(value, timeout) end
185183
---
186184
---Returns `value, true` on success, `nil, false` when the channel is closed, or
187185
---`nil, "timeout"` when a timeout is supplied and no value is available.
188-
---@generic T
189186
---@overload fun(self: threading.channel<T>): T, true
190187
---@overload fun(self: threading.channel<T>): nil, false
191188
---@overload fun(self: threading.channel<T>, timeout: number): T, true
@@ -200,7 +197,6 @@ function threading_channel:recv(timeout) end
200197
---
201198
---Returns `value, true` on success, or `nil, false` when the channel is empty
202199
---or closed.
203-
---@generic T
204200
---@overload fun(self: threading.channel<T>): T, true
205201
---@overload fun(self: threading.channel<T>): nil, false
206202
---@return T|nil value
@@ -215,7 +211,7 @@ function threading_channel:close() end
215211
---@nodiscard
216212
function threading_channel:__tostring() end
217213

218-
---@class threadinglib
214+
---@class threading
219215
local threading = {}
220216

221217
---@return integer count
@@ -254,7 +250,6 @@ function threading.gcmode(mode) end
254250
---@param fn threading.thread_fn
255251
---@param ... any
256252
---@return threading.thread thread
257-
---@nodiscard
258253
function threading.spawn(fn, ...) end
259254

260255
---@return threading.thread thread
@@ -269,10 +264,8 @@ function threading.mutex() end
269264
---
270265
---Capacity 0 creates a rendezvous channel. Buffered channels have at least the
271266
---requested capacity.
272-
---@generic T
273267
---@param capacity? integer buffered slot count; defaults to 0.
274-
---@return threading.channel<T> channel
275-
---@nodiscard
268+
---@return threading.channel<any> channel
276269
function threading.channel(capacity) end
277270

278271
return threading

notes/m3.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2410,3 +2410,24 @@ GC2 weak drain in-flight completion guard:
24102410
- `LUA=luajit LJ_TEST_DISABLE_BUILD_CACHE=1 tools/ci/lua_test.sh m3_gc2_paranoia`
24112411
- `LUA=luajit LJ_TEST_DISABLE_BUILD_CACHE=1 tools/ci/lua_test.sh m6_jit_gc2_readiness`
24122412
- `make -C src clean && make -C src && LUA=luajit tools/ci/lua_test.sh run_stock_tests -- src/luajit --quiet`
2413+
2414+
GC2 weak write in-flight completion guard:
2415+
2416+
- Added `GC2State.weak_write_active` and public
2417+
`lj_gc2_weak_write_begin/end()` helpers. Mutator stores into weak tables now
2418+
publish an active writer while they pre-mark the weak key/value, CAS-publish
2419+
the slot, and run the post-store weak/parent barrier. Weak snapshot clearing
2420+
and bridge coverage now wait for both active clear reservations and active
2421+
weak writers before declaring the snapshot complete. The weak drain/write
2422+
active increments use acquire-release fetch-adds, matching the release on
2423+
decrement and acquire completion checks.
2424+
- The guard is wired through C API setters/raw setters, VM/JIT table store
2425+
helpers, table-valued `__newindex`, and FFI metatype `__newindex` stores. The
2426+
non-weak table path keeps the previous post-store barrier shape.
2427+
- `t-gc2-traverse` now pins the race by holding `weak_write_active` across a
2428+
`P_WEAK` drain attempt, verifying the clear cursor does not advance and the
2429+
late weak entry survives until the writer releases.
2430+
- Verification:
2431+
- `LUA=luajit LJ_TEST_DISABLE_BUILD_CACHE=1 tools/ci/lua_test.sh m3_gc2_paranoia`
2432+
- `LUA=luajit LJ_TEST_DISABLE_BUILD_CACHE=1 tools/ci/lua_test.sh m8_weak`
2433+
- `LUA=luajit LJ_TEST_DISABLE_BUILD_CACHE=1 tools/ci/lua_test.sh m6_jit_gc2_readiness`

notes/m8.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -941,3 +941,11 @@ Weak modes and current finalizer semantics:
941941
scheduler gate now rejects raw finalizer queue, owner, and counter access in
942942
that fixture too.
943943
Verification: `tools/ci/m3_gc2_worker_scheduler.sh` passed.
944+
- Follow-up coroutine callback-state fix: `lj_gc2_finalizer_dispatch_one()` now
945+
uses `lj_state_resumeclaim()`/`lj_state_dropresumeclaim()` for the outer
946+
callback stack claim. Suspended coroutines normally have `tg_hint == NULL`;
947+
attaching the current TG for the duration of finalizer dispatch gives the x64
948+
VM call prologue a valid per-TG dispatch table while preserving the
949+
resume-release cleanup used by normal coroutine resume.
950+
Verification: `LUA=luajit LJ_TEST_DISABLE_BUILD_CACHE=1 tools/ci/lua_test.sh
951+
m8_weak` passed.

plan/05_gc_concurrent.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,12 @@ weak-value array insertion both keep late `P_WEAK` keys/values alive through
486486
the same bridge. During weak clearing, a current-cycle GC2 mark produced by one
487487
of these late writes wins over stale legacy white while `GCSatomic` remains
488488
open, so queued weak snapshots cannot clear a value that was just rescued by the
489-
P_WEAK bridge.
489+
P_WEAK bridge. Mutator stores into weak tables also hold
490+
`GC2State.weak_write_active` from the pre-publication weak barrier until the
491+
post-publication weak/parent barrier is complete. Bounded weak clearing checks
492+
that counter before and after reserving a clear cursor batch, and snapshot
493+
completion/bridge coverage require it to reach zero, so a weak clear cannot race
494+
past a just-published but not-yet-post-barriered weak entry.
490495
`weak_keys_marked` and `weak_values_marked` expose first-time marks from these
491496
bridges for follow-up tests. x64 VM single-value fast table stores now route
492497
their GC2 barrier through `lj_gc2_barrier_tv_pair_g()` with the destination
@@ -550,8 +555,12 @@ instead of waiting forever for `mt_live == 0`, and explicit
550555
`collectgarbage("step", ...)` keeps `GCSfinalize` open instead of reporting a
551556
completed cycle while that worker is still live. User finalizer callbacks now
552557
run on the owner-claimed collector caller `lua_State` instead of the shared
553-
`vmthread(g)` stack. Pending finalizer objects are retained through dedicated
554-
GC2 queue nodes and marked through `lj_gc2_finalizer_mark_all()`, whose GC2
558+
`vmthread(g)` stack. The dispatch claim uses the same resume-style TG attachment
559+
as coroutine resume, so suspended coroutine callback stacks have a valid
560+
`tg_hint` while the VM enters the `__gc` function and are detached again when
561+
the finalizer dispatch claim is released. Pending finalizer objects are retained
562+
through dedicated GC2 queue nodes and marked through
563+
`lj_gc2_finalizer_mark_all()`, whose GC2
555564
side owns the owner-drained queue scanner, but full
556565
scheduler-owned string/root/finalizer sweep driving and FINREG/finqueue
557566
execution remain follow-up work. Legacy root-chain splice retry losers and GC2

src/lib_ffi.c

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,9 +1007,22 @@ static int ffi_index_meta(lua_State *L, CTState *cts, CTypeID id, MMS mm)
10071007
GCtab *owner;
10081008
TValue *o = lj_meta_tset_owner(L, tv, base+1, &owner);
10091009
if (o) {
1010+
int weakwr = lj_gc2_weak_write_begin(L, owner);
1011+
if (weakwr) {
1012+
lj_gc2_barrier_weak_key(L, owner, base+1);
1013+
lj_gc2_barrier_weak_value(L, owner, base+2);
1014+
lj_gc2_barrier_tv_pair(L, obj2gco(owner), base+2);
1015+
}
10101016
copyTVrel(L, o, base+2);
1011-
lj_gc2_barrier_weak_write(L, owner, base+1, base+2);
1012-
lj_gc2_barrier_tv_pair(L, obj2gco(owner), o);
1017+
if (weakwr) {
1018+
lj_gc2_barrier_weak_key(L, owner, base+1);
1019+
lj_gc2_barrier_weak_value(L, owner, base+2);
1020+
lj_gc2_barrier_tv_pair(L, obj2gco(owner), base+2);
1021+
lj_gc2_weak_write_end(L, weakwr);
1022+
} else {
1023+
lj_gc2_barrier_weak_write(L, owner, base+1, base+2);
1024+
lj_gc2_barrier_tv_pair(L, obj2gco(owner), o);
1025+
}
10131026
return 0;
10141027
}
10151028
}

src/lj_api.c

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,10 +1167,21 @@ LUA_API void lua_settable(lua_State *L, int idx)
11671167
o = lj_meta_tset_owner(L, t, L->top-2, &owner);
11681168
if (o) {
11691169
TValue *key = L->top-2, *val = L->top-1;
1170-
if (lj_tab_trystoretv_cas_keyed(L, owner, o, key, val) ==
1171-
LJ_TAB_STORE_CAS_OK) {
1170+
int weakwr = lj_gc2_weak_write_begin(L, owner);
1171+
int rc;
1172+
if (weakwr)
11721173
lj_gc2_barrier_weak_write(L, owner, key, val);
1173-
lj_gc2_barrier_tv_pair(L, obj2gco(owner), o);
1174+
rc = lj_tab_trystoretv_cas_keyed(L, owner, o, key, val);
1175+
if (weakwr) {
1176+
lj_gc2_barrier_weak_write(L, owner, key, val);
1177+
lj_gc2_barrier_tv_pair(L, obj2gco(owner), val);
1178+
lj_gc2_weak_write_end(L, weakwr);
1179+
}
1180+
if (rc == LJ_TAB_STORE_CAS_OK) {
1181+
if (!weakwr) {
1182+
lj_gc2_barrier_weak_write(L, owner, key, val);
1183+
lj_gc2_barrier_tv_pair(L, obj2gco(owner), o);
1184+
}
11741185
L->top = key;
11751186
return;
11761187
}
@@ -1199,10 +1210,21 @@ LUA_API void lua_setfield(lua_State *L, int idx, const char *k)
11991210
o = lj_meta_tset_owner(L, t, &key, &owner);
12001211
if (o) {
12011212
TValue *val = L->top-1;
1202-
if (lj_tab_trystoretv_cas_keyed(L, owner, o, &key, val) ==
1203-
LJ_TAB_STORE_CAS_OK) {
1213+
int weakwr = lj_gc2_weak_write_begin(L, owner);
1214+
int rc;
1215+
if (weakwr)
12041216
lj_gc2_barrier_weak_write(L, owner, &key, val);
1205-
lj_gc2_barrier_tv_pair(L, obj2gco(owner), o);
1217+
rc = lj_tab_trystoretv_cas_keyed(L, owner, o, &key, val);
1218+
if (weakwr) {
1219+
lj_gc2_barrier_weak_write(L, owner, &key, val);
1220+
lj_gc2_barrier_tv_pair(L, obj2gco(owner), val);
1221+
lj_gc2_weak_write_end(L, weakwr);
1222+
}
1223+
if (rc == LJ_TAB_STORE_CAS_OK) {
1224+
if (!weakwr) {
1225+
lj_gc2_barrier_weak_write(L, owner, &key, val);
1226+
lj_gc2_barrier_tv_pair(L, obj2gco(owner), o);
1227+
}
12061228
L->top = val;
12071229
return;
12081230
}
@@ -1223,17 +1245,32 @@ LUA_API void lua_rawset(lua_State *L, int idx)
12231245
TValue snap;
12241246
GCtab *t = tabV(index2adr_read(L, idx, &snap));
12251247
TValue *dst, *key;
1248+
int barrier_done = 0;
12261249
lj_checkapi_slot(2);
12271250
key = L->top-2;
12281251
for (;;) {
1252+
int weakwr, rc;
12291253
dst = lj_tab_set(L, t, key);
1230-
if (lj_tab_trystoretv_cas_keyed(L, t, dst, key, key+1) ==
1231-
LJ_TAB_STORE_CAS_OK)
1254+
weakwr = lj_gc2_weak_write_begin(L, t);
1255+
if (weakwr)
1256+
lj_gc2_barrier_weak_write(L, t, key, key+1);
1257+
rc = lj_tab_trystoretv_cas_keyed(L, t, dst, key, key+1);
1258+
if (weakwr) {
1259+
lj_gc2_barrier_weak_write(L, t, key, key+1);
1260+
lj_gc2_barrier_tv_pair(L, obj2gco(t), key+1);
1261+
lj_gc_pubtab(L, t);
1262+
lj_gc2_weak_write_end(L, weakwr);
1263+
if (rc == LJ_TAB_STORE_CAS_OK)
1264+
barrier_done = 1;
1265+
}
1266+
if (rc == LJ_TAB_STORE_CAS_OK)
12321267
break;
12331268
lj_tab_store_wait_l(L); /* C API rawset saw stale/FORWARD slot. */
12341269
}
1235-
lj_gc2_barrier_weak_write(L, t, key, key+1);
1236-
lj_gc_pubtab(L, t);
1270+
if (!barrier_done) {
1271+
lj_gc2_barrier_weak_write(L, t, key, key+1);
1272+
lj_gc_pubtab(L, t);
1273+
}
12371274
L->top = key;
12381275
}
12391276

@@ -1243,18 +1280,33 @@ LUA_API void lua_rawseti(lua_State *L, int idx, int n)
12431280
GCtab *t = tabV(index2adr_read(L, idx, &snap));
12441281
TValue *dst, *src;
12451282
TValue key;
1283+
int barrier_done = 0;
12461284
lj_checkapi_slot(1);
12471285
src = L->top-1;
12481286
setintV(&key, n);
12491287
for (;;) {
1288+
int weakwr, rc;
12501289
dst = lj_tab_setint(L, t, n);
1251-
if (lj_tab_trystoretv_cas_keyed(L, t, dst, &key, src) ==
1252-
LJ_TAB_STORE_CAS_OK)
1290+
weakwr = lj_gc2_weak_write_begin(L, t);
1291+
if (weakwr)
1292+
lj_gc2_barrier_weak_write(L, t, &key, src);
1293+
rc = lj_tab_trystoretv_cas_keyed(L, t, dst, &key, src);
1294+
if (weakwr) {
1295+
lj_gc2_barrier_weak_write(L, t, &key, src);
1296+
lj_gc2_barrier_tv_pair(L, obj2gco(t), src);
1297+
lj_gc_pubtabtv(L, t, dst);
1298+
lj_gc2_weak_write_end(L, weakwr);
1299+
if (rc == LJ_TAB_STORE_CAS_OK)
1300+
barrier_done = 1;
1301+
}
1302+
if (rc == LJ_TAB_STORE_CAS_OK)
12531303
break;
12541304
lj_tab_store_wait_l(L); /* C API rawseti saw stale/FORWARD slot. */
12551305
}
1256-
lj_gc2_barrier_weak_write(L, t, &key, src);
1257-
lj_gc_pubtabtv(L, t, dst);
1306+
if (!barrier_done) {
1307+
lj_gc2_barrier_weak_write(L, t, &key, src);
1308+
lj_gc_pubtabtv(L, t, dst);
1309+
}
12581310
L->top = src;
12591311
}
12601312

src/lj_atomic.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ LA_INLINE int la_cas128(la_u128 *p, la_u128 *exp, la_u128 des)
9595

9696
/* ---- fetch ops ------------------------------------------------------ */
9797
LA_INLINE uint32_t la_add32_rlx(uint32_t *p,uint32_t v){return __atomic_fetch_add(p,v,__ATOMIC_RELAXED);}
98+
LA_INLINE uint32_t la_add32_acqrel(uint32_t *p,uint32_t v){return __atomic_fetch_add(p,v,__ATOMIC_ACQ_REL);}
9899
LA_INLINE uint64_t la_add64_rlx(uint64_t *p,uint64_t v){return __atomic_fetch_add(p,v,__ATOMIC_RELAXED);}
99100
LA_INLINE uint32_t la_sub32_rlx(uint32_t *p,uint32_t v){return __atomic_fetch_sub(p,v,__ATOMIC_RELAXED);}
100101
LA_INLINE uint64_t la_sub64_rlx(uint64_t *p,uint64_t v){return __atomic_fetch_sub(p,v,__ATOMIC_RELAXED);}

src/lj_gc.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,8 @@ LJ_FUNC void lj_gc2_barrier_weak_key(lua_State *L, GCtab *t, cTValue *key);
300300
LJ_FUNC void lj_gc2_barrier_weak_value(lua_State *L, GCtab *t, cTValue *val);
301301
LJ_FUNC void lj_gc2_barrier_weak_write(lua_State *L, GCtab *t, cTValue *key,
302302
cTValue *val);
303+
LJ_FUNC int lj_gc2_weak_write_begin(lua_State *L, GCtab *t);
304+
LJ_FUNC void lj_gc2_weak_write_end(lua_State *L, int active);
303305
LJ_FUNCA void lj_gc_barrierback_tab_g(global_State *g, GCtab *t);
304306

305307
/* Move the GC propagation frontier back for tables (make it gray again). */

0 commit comments

Comments
 (0)