Skip to content

Commit 1504133

Browse files
committed
defrag: give the module global defrag cursor a server-managed lifetime
The global defrag cursor was an unsigned long living in the module structure with no lifetime of its own. The per-key cursor does not need one, because it is bracketed by a single key and moduleLateDefrag() resets it when the callback reports completion. The global callback has no such bracketing, so a position saved mid-pass could be handed back after the state it described had been rebuilt. Make it a core-owned object instead. moduleDefragCursor is allocated when the server first visits a module in a cycle and freed when the pass completes, when the cycle terminates abnormally, or when the module is unloaded. Freeing it is what makes a stale resume impossible: a cursor of 0 now unambiguously means a fresh pass. The module-visible API is unchanged. The defrag context points at the position inside the cursor, so VM_DefragCursorSet()/VM_DefragCursorGet() work exactly as before and valkeymodule.h is untouched - no new calls, no altered signatures, nothing for an existing module to adapt to. Naming follows the internal convention (moduleType, moduleValue) rather than the ValkeyModule* prefix reserved for API types. Making it a type rather than a bare value is what allows further per-pass state to be added later without changing any signature. Adds a test for unloading a module while a pass is outstanding. Signed-off-by: AkshaThakkar1812 <akshathakkar@gmail.com>
1 parent c880fff commit 1504133

3 files changed

Lines changed: 76 additions & 9 deletions

File tree

src/module.c

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2449,7 +2449,7 @@ void VM_SetModuleAttribs(ValkeyModuleCtx *ctx, const char *name, int ver, int ap
24492449
module->options = 0;
24502450
module->info_cb = 0;
24512451
module->defrag_cb = 0;
2452-
module->defrag_cursor = 0;
2452+
module->defrag_cursor = NULL;
24532453
module->defrag_done_this_cycle = 0;
24542454
module->loadmod = NULL;
24552455
module->num_commands_with_acl_categories = 0;
@@ -13226,6 +13226,8 @@ void moduleLoadFromQueue(void) {
1322613226
}
1322713227

1322813228
void moduleFreeModuleStructure(struct ValkeyModule *module) {
13229+
/* A global defrag pass may be outstanding, and its cursor is owned by the server. */
13230+
moduleDefragGlobalsReleaseCursor(module);
1322913231
listRelease(module->types);
1323013232
listRelease(module->filters);
1323113233
listRelease(module->usedby);
@@ -14986,6 +14988,32 @@ struct ValkeyModuleDefragCtx {
1498614988
int dbid; /* The dbid of the key being processed, -1 when unknown. */
1498714989
};
1498814990

14991+
/* Resume state for a module's global defrag pass.
14992+
*
14993+
* The per-key defrag cursor needs no lifetime of its own: it belongs to one key, and the server
14994+
* resets it in moduleLateDefrag() when that key's callback reports completion. The global callback
14995+
* has no such bracketing, so its cursor is an object the server allocates when it first visits the
14996+
* module in a cycle and frees when the pass ends, when the cycle terminates abnormally, or when the
14997+
* module is unloaded. Freeing it is what guarantees a module is never handed a position saved
14998+
* before an interruption.
14999+
*
15000+
* The position is reached through the existing VM_DefragCursorSet()/VM_DefragCursorGet(), which the
15001+
* defrag context points at, so this type adds no module-visible API. Making it a type rather than a
15002+
* bare value means further per-pass state can be added here without changing any signature. */
15003+
typedef struct moduleDefragCursor {
15004+
unsigned long position; /* Where the callback stopped. Zero means done for this cycle. */
15005+
} moduleDefragCursor;
15006+
15007+
static moduleDefragCursor *moduleDefragCursorCreate(void) {
15008+
moduleDefragCursor *cursor = zmalloc(sizeof(*cursor));
15009+
cursor->position = 0;
15010+
return cursor;
15011+
}
15012+
15013+
static void moduleDefragCursorDestroy(moduleDefragCursor *cursor) {
15014+
zfree(cursor);
15015+
}
15016+
1498915017
/* Register a defrag callback for global data, i.e. anything that the module
1499015018
* may allocate that is not tied to a specific data type.
1499115019
*
@@ -14996,6 +15024,10 @@ struct ValkeyModuleDefragCtx {
1499615024
* means "done"; a non-zero cursor tells the defrag process there is more work
1499715025
* and the callback will be invoked again. The callback MUST store a cursor of 0
1499815026
* once it has finished, otherwise it will keep being invoked.
15027+
*
15028+
* The cursor belongs to one pass and is owned by the server, which discards it once the callback
15029+
* reports completion or the pass is interrupted. A callback therefore only ever reads back a
15030+
* position it stored during the same pass, and a cursor of 0 always means a fresh start.
1499915031
*/
1500015032
int VM_RegisterDefragFunc(ValkeyModuleCtx *ctx, ValkeyModuleDefragFunc cb) {
1500115033
ctx->module->defrag_cb = cb;
@@ -15171,10 +15203,11 @@ static unsigned long defrag_module_start_idx = 0;
1517115203
/* Called at stage init (endtime==0) to start a new global defrag pass. Clears each module's
1517215204
* done flag so every module is visited again, and resets the round-robin start position.
1517315205
*
15174-
* Cursors are not cleared here. On a normal cycle end every cursor is already 0, because the
15175-
* stage only reports DEFRAG_DONE once no module has work left. A cursor can only outlive a cycle
15176-
* when that cycle was aborted, and that case is handled in moduleDefragGlobalsAbort() rather than
15177-
* here, so a module is never handed back a position saved by an interrupted pass. */
15206+
* Cursors are not released here. On a normal cycle end every cursor has already been released,
15207+
* because the stage only reports DEFRAG_DONE once no module has work left. A cursor can only
15208+
* outlive a cycle when that cycle was aborted, and that case is handled in
15209+
* moduleDefragGlobalsAbort() rather than here, so a module is never handed back a position saved by
15210+
* an interrupted pass. */
1517815211
void moduleDefragGlobalsStart(void) {
1517915212
defrag_module_start_idx = 0;
1518015213

@@ -15188,6 +15221,13 @@ void moduleDefragGlobalsStart(void) {
1518815221
}
1518915222
}
1519015223

15224+
/* Free a module's global defrag cursor. Called when the pass ends, when it is interrupted, and when
15225+
* the module is unloaded. */
15226+
void moduleDefragGlobalsReleaseCursor(struct ValkeyModule *module) {
15227+
moduleDefragCursorDestroy(module->defrag_cursor);
15228+
module->defrag_cursor = NULL;
15229+
}
15230+
1519115231
/* Discard in-progress global defrag state. Called when a defrag cycle terminates abnormally,
1519215232
* which interrupts modules mid-pass: a cursor saved at that point refers to a position in module
1519315233
* state that may be rebuilt before defrag next runs, so it must not be handed back. Mirrors
@@ -15199,7 +15239,7 @@ void moduleDefragGlobalsAbort(void) {
1519915239
listRewind(modules, &li);
1520015240
while ((ln = listNext(&li)) != NULL) {
1520115241
struct ValkeyModule *module = listNodeValue(ln);
15202-
module->defrag_cursor = 0;
15242+
moduleDefragGlobalsReleaseCursor(module);
1520315243
module->defrag_done_this_cycle = 0;
1520415244
}
1520515245
}
@@ -15242,11 +15282,16 @@ int moduleDefragGlobals(monotime endtime) {
1524215282
struct ValkeyModule *module = listNodeValue(ln);
1524315283
if (!module->defrag_cb) continue;
1524415284
if (module->defrag_done_this_cycle) continue;
15245-
ValkeyModuleDefragCtx defrag_ctx = {endtime, &module->defrag_cursor, NULL, -1};
15285+
/* Allocate the cursor on the first visit of this cycle and point the context at the
15286+
* position inside it, so VM_DefragCursorSet()/Get() work unchanged. */
15287+
if (!module->defrag_cursor) module->defrag_cursor = moduleDefragCursorCreate();
15288+
ValkeyModuleDefragCtx defrag_ctx = {endtime, &module->defrag_cursor->position, NULL, -1};
1524615289
module->defrag_cb(&defrag_ctx);
15247-
if (module->defrag_cursor != 0) {
15290+
if (module->defrag_cursor->position != 0) {
1524815291
more_work = 1;
1524915292
} else {
15293+
/* Done for this cycle, so the pass is over and its cursor goes with it. */
15294+
moduleDefragGlobalsReleaseCursor(module);
1525015295
module->defrag_done_this_cycle = 1;
1525115296
}
1525215297
if (endtime != 0 && getMonotonicUs() >= endtime) {

src/module.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ typedef struct moduleValue {
9999
typedef int (*ModuleLoadFunc)(void *, void **, int);
100100
typedef int (*ModuleUnLoadFunc)(void *);
101101

102+
/* Resume state for a module's global defrag pass, defined in module.c. */
103+
typedef struct moduleDefragCursor moduleDefragCursor;
104+
102105
/* This structure represents a module inside the system. */
103106
typedef struct ValkeyModule {
104107
void *handle; /* Module dlopen() handle. */
@@ -117,7 +120,7 @@ typedef struct ValkeyModule {
117120
int blocked_clients; /* Count of ValkeyModuleBlockedClient in this module. */
118121
ValkeyModuleInfoFunc info_cb; /* Callback for module to add INFO fields. */
119122
ValkeyModuleDefragFunc defrag_cb; /* Callback for global data defrag. */
120-
unsigned long defrag_cursor; /* Global defrag cursor, owned by the module, persists across cycles. */
123+
moduleDefragCursor *defrag_cursor; /* Global defrag: resume state for the current pass. */
121124
int defrag_done_this_cycle; /* Global defrag: module is done this cycle, skip until next cycle. */
122125
struct moduleLoadQueueEntry *loadmod; /* Module load arguments for config rewrite. */
123126
int num_commands_with_acl_categories; /* Number of commands in this module included in acl categories */
@@ -246,6 +249,7 @@ int moduleDefragValue(robj *key, robj *obj, int dbid);
246249
int moduleLateDefrag(robj *key, robj *value, unsigned long *cursor, monotime endtime, int dbid);
247250
void moduleDefragGlobalsStart(void);
248251
void moduleDefragGlobalsAbort(void);
252+
void moduleDefragGlobalsReleaseCursor(struct ValkeyModule *module);
249253
int moduleDefragGlobals(monotime endtime);
250254
void *moduleGetHandleByName(char *modulename);
251255
int moduleIsModuleCommand(void *module_handle, struct serverCommand *cmd);

tests/unit/moduleapi/defrag.tcl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,5 +125,23 @@ start_server {tags {"modules"} overrides {{save ""}}} {
125125
fail "module cursor was not discarded when the defrag cycle was aborted"
126126
}
127127
}
128+
129+
test {Module defrag: unloading a module releases an outstanding cursor} {
130+
# The busy module never finishes, so it always has a cursor allocated. Unloading it must
131+
# free that cursor rather than leak it, and must not leave the defrag stage referring to
132+
# a module that is gone.
133+
assert {[getInfoProperty [r info defragglobalbusy_stats] \
134+
defragglobalbusy_busy_calls] > 0}
135+
136+
assert_equal {OK} [r module unload defragglobalbusy]
137+
138+
# The remaining module must keep being defragged afterwards.
139+
r frag.resetstats
140+
wait_for_condition 50 100 {
141+
[getInfoProperty [r info defragtest_stats] defragtest_global_attempts] > 0
142+
} else {
143+
fail "global defrag stopped working after a module was unloaded"
144+
}
145+
}
128146
}
129147
}

0 commit comments

Comments
 (0)