-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Making the module global-defrag callback usable #4487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: unstable
Are you sure you want to change the base?
Changes from all commits
49ebeb1
0d1e58f
3c99401
aad1e2d
180b9fc
76dd117
13d8c39
66176ad
8be3317
03e646c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2449,6 +2449,8 @@ void VM_SetModuleAttribs(ValkeyModuleCtx *ctx, const char *name, int ver, int ap | |
| module->options = 0; | ||
| module->info_cb = 0; | ||
| module->defrag_cb = 0; | ||
| module->defrag_cursor = 0; | ||
| module->defrag_done_this_cycle = 0; | ||
| module->loadmod = NULL; | ||
| module->num_commands_with_acl_categories = 0; | ||
| module->onload = 1; | ||
|
|
@@ -14986,22 +14988,32 @@ struct ValkeyModuleDefragCtx { | |
|
|
||
| /* Register a defrag callback for global data, i.e. anything that the module | ||
| * may allocate that is not tied to a specific data type. | ||
| * | ||
| * Unlike the per-key data type defrag callback, the global callback is invoked | ||
| * with a time limit: it should call VM_DefragShouldStop() periodically and | ||
| * return once that returns non-zero. To resume where it left off on the next | ||
| * call, it uses VM_DefragCursorSet()/VM_DefragCursorGet(). A stored cursor of 0 | ||
| * means "done"; a non-zero cursor tells the defrag process there is more work | ||
| * and the callback will be invoked again. The callback MUST store a cursor of 0 | ||
| * once it has finished, otherwise it will keep being invoked. | ||
| */ | ||
| int VM_RegisterDefragFunc(ValkeyModuleCtx *ctx, ValkeyModuleDefragFunc cb) { | ||
| ctx->module->defrag_cb = cb; | ||
| return VALKEYMODULE_OK; | ||
| } | ||
|
|
||
| /* When the data type defrag callback iterates complex structures, this | ||
| * function should be called periodically. A zero (false) return | ||
| * indicates the callback may continue its work. A non-zero value (true) | ||
| * indicates it should stop. | ||
| /* When a defrag callback iterates complex structures, this function should be | ||
| * called periodically. A zero (false) return indicates the callback may | ||
| * continue its work. A non-zero value (true) indicates it should stop. | ||
| * | ||
| * When stopped, the callback may use VM_DefragCursorSet() to store its | ||
| * When stopped, the callback should use VM_DefragCursorSet() to store its | ||
| * position so it can later use VM_DefragCursorGet() to resume defragging. | ||
| * | ||
| * When stopped and more work is left to be done, the callback should | ||
| * return 1. Otherwise, it should return 0. | ||
| * How "more work remains" is signalled depends on the callback type: | ||
| * - the per-key data type defrag callback returns 1 if stopped with more work | ||
| * left, or 0 when done; | ||
| * - the global defrag callback returns nothing; instead a stored cursor of 0 | ||
| * means done and a non-zero cursor means more work remains. | ||
| * | ||
| * NOTE: Modules should consider the frequency in which this function is called, | ||
| * so it generally makes sense to do small batches of work in between calls. | ||
|
|
@@ -15012,25 +15024,25 @@ int VM_DefragShouldStop(ValkeyModuleDefragCtx *ctx) { | |
|
|
||
| /* Store an arbitrary cursor value for future re-use. | ||
| * | ||
| * This should only be called if VM_DefragShouldStop() has returned a non-zero | ||
| * value and the defrag callback is about to exit without fully iterating its | ||
| * data type. | ||
| * | ||
| * This behavior is reserved to cases where late defrag is performed. Late | ||
| * defrag is selected for keys that implement the `free_effort` callback and | ||
| * return a `free_effort` value that is larger than the defrag | ||
| * 'active-defrag-max-scan-fields' configuration directive. | ||
| * | ||
| * Smaller keys, keys that do not implement `free_effort` or the global | ||
| * defrag callback are not called in late-defrag mode. In those cases, a | ||
| * call to this function will return VALKEYMODULE_ERR. | ||
| * | ||
| * The cursor may be used by the module to represent some progress into the | ||
| * module's data type. Modules may also store additional cursor-related | ||
| * information locally and use the cursor as a flag that indicates when | ||
| * traversal of a new key begins. This is possible because the API makes | ||
| * a guarantee that concurrent defragmentation of multiple keys will | ||
| * not be performed. | ||
| * This is used to resume defragmentation across callback invocations, and is | ||
| * available in two cases: | ||
| * - "late defrag" of a data type key. Late defrag is selected for keys that | ||
| * implement the `free_effort` callback and return a value larger than the | ||
| * 'active-defrag-max-scan-fields' configuration directive. Smaller keys, and | ||
| * keys that do not implement `free_effort`, are not defragged in late mode, | ||
| * and a call to this function for them returns VALKEYMODULE_ERR. | ||
| * - the global defrag callback (registered via VM_RegisterDefragFunc), which | ||
| * is always given a cursor. There, a stored cursor of 0 means the callback | ||
| * is done and a non-zero value means it should be invoked again to continue. | ||
| * | ||
| * The cursor may be used by the module to represent some progress into its | ||
| * data. Modules may also store additional cursor-related information locally | ||
| * and use the cursor as a flag that indicates when traversal of a new key | ||
| * begins. This is possible because the API guarantees that concurrent | ||
| * defragmentation of multiple keys will not be performed. | ||
| * | ||
| * Returns VALKEYMODULE_ERR if no cursor is available for this callback (see | ||
| * above), VALKEYMODULE_OK otherwise. | ||
| */ | ||
| int VM_DefragCursorSet(ValkeyModuleDefragCtx *ctx, unsigned long cursor) { | ||
| if (!ctx->cursor) return VALKEYMODULE_ERR; | ||
|
|
@@ -15041,9 +15053,9 @@ int VM_DefragCursorSet(ValkeyModuleDefragCtx *ctx, unsigned long cursor) { | |
|
|
||
| /* Fetch a cursor value that has been previously stored using VM_DefragCursorSet(). | ||
| * | ||
| * If not called for a late defrag operation, VALKEYMODULE_ERR will be returned and | ||
| * the cursor should be ignored. See VM_DefragCursorSet() for more details on | ||
| * defrag cursors. | ||
| * Returns VALKEYMODULE_ERR if no cursor is available for this callback (see | ||
| * VM_DefragCursorSet() for when that is the case), in which case the cursor | ||
| * should be ignored. On the first invocation the stored cursor is 0. | ||
| */ | ||
| int VM_DefragCursorGet(ValkeyModuleDefragCtx *ctx, unsigned long *cursor) { | ||
| if (!ctx->cursor) return VALKEYMODULE_ERR; | ||
|
|
@@ -15145,20 +15157,64 @@ int moduleDefragValue(robj *key, robj *value, int dbid) { | |
| return 1; | ||
| } | ||
|
|
||
| /* Call registered module API defrag functions */ | ||
| void moduleDefragGlobals(void) { | ||
| if (listLength(modules) == 0) return; | ||
| /* Index of the module to start from on the next moduleDefragGlobals() call. Advanced past the | ||
| * module we stopped on so a module with ongoing work doesn't starve the others. */ | ||
| static unsigned long defrag_module_start_idx = 0; | ||
|
|
||
| /* Called at stage init (endtime==0) to start a new global defrag pass. Clears each module's | ||
| * done flag so every module is visited again, and resets the round-robin start position. Cursors | ||
| * are not touched here: a module owns its cursor and may carry progress across cycles. */ | ||
| void moduleDefragGlobalsStart(void) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The module cursor is never reset, unlike every other defrag cursor.
|
||
| defrag_module_start_idx = 0; | ||
|
|
||
| listIter li; | ||
| listNode *ln; | ||
|
|
||
| listRewind(modules, &li); | ||
| while ((ln = listNext(&li)) != NULL) { | ||
| struct ValkeyModule *module = listNodeValue(ln); | ||
| module->defrag_done_this_cycle = 0; | ||
| } | ||
| } | ||
|
|
||
| /* Invoke each module's global defrag callback, forwarding 'endtime' so the callback can bound its | ||
| * own latency via VM_DefragShouldStop(). Each module is given a persistent cursor | ||
| * (module->defrag_cursor) to save progress with VM_DefragCursorSet() and resume on a later call. | ||
| * | ||
| * The cursor is also the module's "more work" signal, following the convention used elsewhere in | ||
| * defrag: a non-zero cursor means the module wants to be called again (scan not finished, or work | ||
| * still draining on its own threads); a zero cursor means it is done for this cycle. A module done | ||
| * this cycle sets defrag_done_this_cycle and is skipped until the next cycle clears it. | ||
| * | ||
| * When the deadline is hit mid-iteration we resume on the next call from the module after the one | ||
| * we stopped on (defrag_module_start_idx), so a module that keeps consuming the deadline can't | ||
| * starve the modules after it. The done flags and start index live outside the module structs, so | ||
| * they are unaffected if a module is unloaded between calls. | ||
| * | ||
| * Returns 1 if any module still has work to do, 0 otherwise. */ | ||
| int moduleDefragGlobals(monotime endtime) { | ||
| int more_work = 0; | ||
| unsigned long count = listLength(modules); | ||
| if (count == 0) return more_work; | ||
|
|
||
| for (unsigned long n = 0; n < count; n++) { | ||
| unsigned long idx = (defrag_module_start_idx + n) % count; | ||
| struct ValkeyModule *module = listNodeValue(listIndex(modules, idx)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if (!module->defrag_cb) continue; | ||
| ValkeyModuleDefragCtx defrag_ctx = {0, NULL, NULL, -1}; | ||
| if (module->defrag_done_this_cycle) continue; | ||
| ValkeyModuleDefragCtx defrag_ctx = {endtime, &module->defrag_cursor, NULL, -1}; | ||
| module->defrag_cb(&defrag_ctx); | ||
| if (module->defrag_cursor != 0) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated some comments |
||
| more_work = 1; | ||
| } else { | ||
| module->defrag_done_this_cycle = 1; | ||
| } | ||
| if (endtime != 0 && getMonotonicUs() >= endtime) { | ||
| defrag_module_start_idx = (idx + 1) % count; | ||
| break; | ||
| } | ||
| } | ||
| return more_work; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| /* Returns the name of the key currently being processed. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| /* A module whose global defrag callback never finishes: it consumes the whole | ||
| * deadline on every invocation and always leaves a non-zero cursor. Used | ||
| * together with defragtest to check that such a module does not starve the | ||
| * global defrag callbacks of other modules (see defrag.tcl). | ||
| */ | ||
|
|
||
| #include "valkeymodule.h" | ||
|
|
||
| /* Number of times our global defrag callback was invoked. Exposed via INFO so | ||
| * the test can confirm the busy module actually ran. */ | ||
| unsigned long long busy_calls = 0; | ||
|
|
||
| static void defragBusyGlobal(ValkeyModuleDefragCtx *ctx) { | ||
| busy_calls++; | ||
| /* Burn the rest of the deadline, then report we still have work by leaving | ||
| * a non-zero cursor. This models a module that never drains within a | ||
| * single defrag cycle. */ | ||
| while (!ValkeyModule_DefragShouldStop(ctx)) { | ||
| /* spin until the deadline is reached */ | ||
| } | ||
| ValkeyModule_DefragCursorSet(ctx, 1); | ||
| } | ||
|
|
||
| static void BusyInfo(ValkeyModuleInfoCtx *ctx, int for_crash_report) { | ||
| VALKEYMODULE_NOT_USED(for_crash_report); | ||
| ValkeyModule_InfoAddSection(ctx, "stats"); | ||
| ValkeyModule_InfoAddFieldULongLong(ctx, "busy_calls", busy_calls); | ||
| } | ||
|
|
||
| int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) { | ||
| VALKEYMODULE_NOT_USED(argv); | ||
| VALKEYMODULE_NOT_USED(argc); | ||
|
|
||
| if (ValkeyModule_Init(ctx, "defragglobalbusy", 1, VALKEYMODULE_APIVER_1) == VALKEYMODULE_ERR) | ||
| return VALKEYMODULE_ERR; | ||
|
|
||
| ValkeyModule_RegisterInfoFunc(ctx, BusyInfo); | ||
| ValkeyModule_RegisterDefragFunc(ctx, defragBusyGlobal); | ||
|
|
||
| return VALKEYMODULE_OK; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A module that never finishes stops all active defrag, not just its own stage.
defragModuleGlobalsis the last stage added inbeginDefragCycle(src/defrag.c:1232). If a module keeps its cursor non-zero,more_workstays 1. The stage then never returnsDEFRAG_DONE,defrag.current_stageis never freed,haveMoreWorkstays true inactiveDefragTimeProc(src/defrag.c:1168), andendDefragCycle(true)is never called. The effects last for the whole life of the process:defragStageDbKeys, expires,keys_with_volatile_items, pubsub, Lua) run only once and never again, because a new cycle never starts. So the feature this PR extends stops doing its main job.server.active_defrag_cpu_percentis never reset to 0 (src/defrag.c:1035). Defrag keeps using up toactive-defrag-cycle-maxof the main thread, even after fragmentation is gone.stat_last_active_defrag_timeis never added tostat_total_active_defrag_time,active_defrag_runningnever goes back to 0 inINFO, and the"Active defrag done in %dms"log line never appears. So an operator gets no signal that something is wrong.