server: Shut down faster by releasing db slices instead of reset - #8239
server: Shut down faster by releasing db slices instead of reset#8239abhijat wants to merge 22 commits into
Conversation
When we are shutting down we do not need to walk every single object and destroy it, the OS will reclaim the heap memory anyway. To avoid wasting time on that a special flag is added. It releases the pointer instead, leaking memory, but it will be reclaimed soon by the OS. Signed-off-by: Abhijat Malviya <abhijat@dragonflydb.io>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
PR Summary by QodoAccelerate server shutdown by releasing database slices
AI Description
Diagram
High-Level Assessment
Files changed (9)
|
Code Review by Qodo🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)
Great, no issues found!Qodo reviewed your code and found no material issues that require reviewTip of the day💡 Did you know, you can route each action level your way: inline, summary, both, or drop Powered by Qodo |
There was a problem hiding this comment.
🟢 Approval recommended
The fast path is confined to final process shutdown while existing test teardown retains deterministic destruction.
Pull request overview
Accelerates production shutdown by intentionally leaking database slices for OS reclamation while retaining normal cleanup for tests.
Changes:
- Adds optional fast-clear propagation through shutdown APIs.
- Releases
DbSliceownership and clears pending asynchronous deletions. - Enables fast clearing in the production shutdown path.
File summaries
| File | Description |
|---|---|
src/server/namespaces.h |
Adds the fast-clear option. |
src/server/namespaces.cc |
Implements slice release during fast shutdown. |
src/server/main_service.h |
Extends the service shutdown API. |
src/server/main_service.cc |
Propagates fast clearing to shards. |
src/server/engine_shard_set.h |
Extends shard-set shutdown API. |
src/server/engine_shard_set.cc |
Passes fast clearing to namespaces. |
src/server/dfly_main.cc |
Enables fast production shutdown. |
src/server/db_slice.h |
Exposes thread-local deleter shutdown. |
src/server/db_slice.cc |
Reuses asynchronous-deleter cleanup. |
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 0
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
🤖 Augment PR SummarySummary: This PR speeds normal server shutdown by intentionally bypassing destruction of per-shard database slices.
🤖 Was this summary useful? React with 👍 or 👎 |
There was a problem hiding this comment.
I wrote this on the issue but this seems like it's the actual root cause for the "deadlock".
It still to me a mystery why we didn't have a CPU spike in some cases or even perf files but the later incidents actually do have those 😮💨
Even though in principle we are correct to exit and let the os reclaim the memory, why not instead use mi_heap_destroy ? It would stop iterating over the whole object space and free all the pages handled by the shard heap. So for O(total_objects) which is potentially huge we would drop down to O(total_mimalloc_pages) which should be handful.
And writing this we can make the same optmization for flushall! Instead of walking down every object destroy the heap in one go and create a new one. This will actually make flushall fast as well
I guess for flushall we should be careful to reinitialize the threadlocals so it might actually be harder in practice but maybe worth looking into ?
P.s. I am not necessarily in favour of using mi_heap_destroy over release && leak memory because we shutdown the instance afterwords. However that might not be true for all environments and I really don't know which one is better because in principle leaking memory to the os and have it reclaiming it is "ok" 🤷 Obviously the downside of what I write here is O(pages) destruct vs O(1). The question though is can O(pages) be slow such that we end up with the same issue ? If the answer is no then it might be worth following to be "in principle correct"
P.s. I am happy to approve as is, just thinking out loud here
Hmm it should be ok even for large datastores, a 500gb datastore with only MI_SMALL_PAGES==64kb should take milliseconds. I am happy to try this if you want me to, then we can be sure and that way we can have |
yes sure, if you can try it out and it works in terms of the time taken, I think it should be fine to go that way. |
|
I prototyped it. The test I added times out when it tries to shut down dragonfly. It's instance (a few ms) to shutdown by reaping the heap pages instead |
|
|
||
| @pytest.mark.large | ||
| @pytest.mark.opt_only | ||
| async def test_shutdown_large_single_hash(df_factory: DflyInstanceFactory): |
There was a problem hiding this comment.
@abhijat if you want you can try it out/stress it more 😄
I guess we won't commit this test, it takes ages but I am leaving it here for reference for now
There was a problem hiding this comment.
🟡 Changes recommended
The resource-intensive manual benchmark should not run automatically in the large test suite.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 1
- Review effort level: Balanced
There was a problem hiding this comment.
🔵 Needs a closer look
Bulk heap destruction intentionally bypasses a broad object graph’s destructors and requires final human validation of allocator ownership and shutdown ordering.
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
The bulk allocator reset leaves stale zmalloc memory accounting when reinitialized.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 1
- Review effort level: Balanced
| void reset_zmalloc_threadlocal(void) { | ||
| zmalloc_heap = NULL; | ||
| } |
There was a problem hiding this comment.
🟡 Changes recommended
Shutdown crashes, allocator correctness gaps, disabled tests, and unsafe CI privileges remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (5)
src/server/engine_shard.cc:577
mi_heap_destroyis still the crash point in the replica-shutdown paths being debugged here: the latest run reports repeated failures inmi_free_generic_mt. Keep the destructor-skipping behavior, but freeshard_and usemi_heap_deleteso mimalloc can safely transfer pages that still participate in ownership bookkeeping.
mi_heap_destroy(tlh);
src/redis/zmalloc_mi.c:233
- Bulk heap teardown bypasses
zfree, so nulling only the heap leaveszmalloc_used_memory_tlcarrying the discarded database usage. A later lifecycle on the same thread then starts with inflated memory/OOM accounting; reset the counter here too.
void reset_zmalloc_threadlocal(void) {
zmalloc_heap = NULL;
}
src/redis/zmalloc.h:153
REDIS_ZMALLOC_MI=OFFselectszmalloc.c, which has no definition for this new API, whileEngineShardcalls it unconditionally. That supported configuration will fail to link; add the no-op fallback implementation there as well.
void reset_zmalloc_threadlocal(void);
.github/workflows/ci.yml:154
- The added
&& falsedisables every C++ unit-test variant, and the regression step is also narrowed to five cases. Restore the normal conditions and remove the temporary test filter before merging this allocator/shutdown change.
if: github.event_name != 'push' && false # debugging fast-shutdown crash: skip unit tests
src/core/topk.cc:87
- With unequal memory resources, this vector move constructs new
HeapItems via the implicit move constructor, so their PMR strings retainother's resource. The destination can then hold dangling allocator pointers; rebuild items with the destination resource or makeHeapItemallocator-aware.
custom_decay_table_ = std::move(other.custom_decay_table_);
counters_ = std::move(other.counters_);
min_heap_ = std::move(other.min_heap_);
- Files reviewed: 30/30 changed files
- Comments generated: 1
- Review effort level: Balanced
Signed-off-by: Kostas Kyrimis <kostas@dragonflydb.io>
Signed-off-by: Kostas Kyrimis <kostas@dragonflydb.io>
There was a problem hiding this comment.
🟡 Changes recommended
The heap-destruction path remains crash-prone, standard C++ tests are disabled, and the regression invocation exits before pytest.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 31/31 changed files
- Comments generated: 1
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
The shutdown path still crashes replica tests, has allocator lifecycle defects, and disables standard CI coverage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (4)
src/server/engine_shard.cc:577
- The focused regression run at this head's shutdown implementation fails multiple replica tests with SIGFPE immediately after
mi_heap_destroystarts. Restore freeingshard_and usemi_heap_delete(tlh)so pages that still participate in mimalloc ownership bookkeeping are transferred safely.
mi_heap_destroy(tlh);
src/redis/zmalloc_mi.c:233
- Bulk heap destruction bypasses
zfree, so this leaveszmalloc_used_memory_tlcarrying usage from the discarded heap. A later lifecycle on the same thread starts with stale memory/OOM accounting; reset the counter here too.
void reset_zmalloc_threadlocal(void) {
zmalloc_heap = NULL;
}
src/redis/zmalloc.h:153
REDIS_ZMALLOC_MI=OFFselectszmalloc.c, which has no definition for this new API, whileEngineShard::DestroyThreadLocalcalls it unconditionally. Add a no-op fallback implementation so that supported configuration still links.
// Drops the thread-local heap pointer. Call before the underlying heap is destroyed
// and before this thread's zmalloc/zfree could be called again (a following
// init_zmalloc_threadlocal() re-arms it for reuse, e.g. across test cycles).
void reset_zmalloc_threadlocal(void);
.github/workflows/ci.yml:87
- This temporary workflow rewrite disables all four C++ unit-test variants and narrows regression coverage to five tests, so the allocator/shutdown changes no longer receive the repository's standard validation. Revert the experimental CI configuration before merge.
# TEMPORARY EXPERIMENT (fast-shutdown SIGFPE investigation): no job-level container:.
# kernel.core_pattern is host-kernel-wide; Docker containers can't write it themselves
# even with extra capabilities (verified: --cap-add SYS_ADMIN alone did not work). So
# this sets it on the bare runner VM, then docker run's the build/test container
# manually so the setting is already in effect before anything inside it can crash.
# Drops ccache/deps-cache (those composite-action steps only work under job-level
# container:) -- intentional, to be reverted after this experiment.
- Files reviewed: 31/31 changed files
- Comments generated: 1
- Review effort level: Balanced
| HeapItem(PMR_NS::memory_resource* mr, std::string_view key, uint32_t count) | ||
| : key(key, PMR_NS::polymorphic_allocator<char>(mr)), count(count) { | ||
| } | ||
|
|
||
| PMR_NS::string key; |
There was a problem hiding this comment.
🔵 Needs a closer look
The shutdown path remains crash-prone, breaks the fallback allocator build, retains stale accounting, and disables C++ CI coverage.
Review details
Suppressed comments (4)
src/redis/zmalloc.h:153
reset_zmalloc_threadlocal()is called unconditionally, butREDIS_ZMALLOC_MI=OFFselectszmalloc.c, which has no definition for this new API, so that supported configuration will fail to link. Add the corresponding fallback implementation inzmalloc.c.
void reset_zmalloc_threadlocal(void);
src/redis/zmalloc_mi.c:233
- Bulk heap destruction bypasses every
zfree, so this thread-local counter retains the bytes from the destroyed heap. Reset it here as well; otherwise a later same-thread initialization reports stale memory usage and may make OOM decisions from the previous server instance.
void reset_zmalloc_threadlocal(void) {
zmalloc_heap = NULL;
}
.github/workflows/ci.yml:184
- The forced
&& falsedisables the entire C++ test suite for every PR matrix entry; the same guard also disables the epoll and both cluster variants below. Remove all four forced-false guards before merge so these allocator and shutdown changes retain required regression coverage.
if: github.event_name != 'push' && false # debugging fast-shutdown crash: skip unit tests
src/server/engine_shard.cc:577
- This switch to
mi_heap_destroyis the fast-shutdown path already producing replica-shutdown/SIGFPE failures, as reflected by the temporary crash-instrumentation changes in this PR. Keep the stablemi_free(shard_)plusmi_heap_delete(tlh)teardown until the allocator crash is fixed rather than merging a shutdown path known to crash.
mi_heap_destroy(tlh);
- Files reviewed: 31/31 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Heap teardown has correctness and build issues, and the normal CI coverage is disabled.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (4)
src/server/engine_shard.cc:577
mi_heap_destroyinvalidates every outstanding allocation immediately, but shutdown still performs thread-local and connection cleanup aftershard_set->Shutdown(); this fast path already correlates with the replica shutdown crashes called out in the workflow. Keep the pointer-release optimization, but freeshard_and usemi_heap_delete(tlh)so late owners can safely release their allocations.
mi_heap_destroy(tlh);
src/redis/zmalloc_mi.c:233
- Bulk heap destruction bypasses every
zfree, so leavingzmalloc_used_memory_tlunchanged carries the destroyed heap's usage into a later initialization on the same thread and corrupts memory accounting. Reset the counter together with the heap pointer.
void reset_zmalloc_threadlocal(void) {
zmalloc_heap = NULL;
}
src/redis/zmalloc.h:153
- This new API is called unconditionally, but the
REDIS_ZMALLOC_MI=OFFbuild selectszmalloc.c, which has noreset_zmalloc_threadlocaldefinition, causing a link failure. Add the corresponding fallback implementation inzmalloc.c.
// Drops the thread-local heap pointer. Call before the underlying heap is destroyed
// and before this thread's zmalloc/zfree could be called again (a following
// init_zmalloc_threadlocal() re-arms it for reuse, e.g. across test cycles).
void reset_zmalloc_threadlocal(void);
.github/workflows/ci.yml:154
- The hard-coded
&& falsedisables all four C++ test suites for every PR matrix entry, while the regression job below is also restricted to five cases. Restore the original guards and full regression selection before merging so this allocator and teardown rewrite receives the repository's normal validation.
- name: C++ Unit Tests - IoUring
if: github.event_name != 'push' && false # debugging fast-shutdown crash: skip unit tests
- Files reviewed: 37/37 changed files
- Comments generated: 1
- Review effort level: Balanced
| LOG(ERROR) << "Service::Shutdown: shard_set->PreShutdown starting"; | ||
| shard_set->PreShutdown(); | ||
| LOG(ERROR) << "Service::Shutdown: shard_set->PreShutdown done, shard_set->Shutdown starting"; | ||
| shard_set->Shutdown(); | ||
| LOG(ERROR) << "Service::Shutdown: shard_set->Shutdown done"; |
There was a problem hiding this comment.
🔵 Needs a closer look
Heap teardown and accounting defects remain, and multiple CI test suites are disabled.
Review details
Suppressed comments (8)
Previously missed (1) — in code that hasn't changed since the last review.
.github/workflows/ci.yml:185
- This condition permanently disables the epoll C++ test suite, including shutdown coverage. Restore the original condition before merging.
This issue also appears in the following locations of the same file:
- line 213
- line 225
src/server/engine_shard.cc:577
mi_heap_destroyforce-reclaims allocations that may still be released by longer-lived shutdown owners; this teardown path is the one associated with the replica shutdown crashes called out by this branch's CI changes. Keep the safemi_heap_delete(tlh)behavior and explicitly freeshard_rather than invalidating the heap eagerly.
mi_heap_destroy(tlh);
src/redis/zmalloc_mi.c:233
- This drops the heap pointer but leaves
zmalloc_used_memory_tlaccounting allocations from the destroyed heap. Tests recreateServerStateon the same worker threads, so the next instance starts with stale memory usage and can make incorrect OOM/usage decisions; reset the counter here too.
void reset_zmalloc_threadlocal(void) {
zmalloc_heap = NULL;
}
src/redis/zmalloc.h:153
REDIS_ZMALLOC_MI=OFFselectszmalloc.c, but that implementation does not define this new function whileEngineShard::DestroyThreadLocal()calls it unconditionally, causing the fallback build to fail at link time. Add the corresponding no-op/reset implementation tozmalloc.c.
void reset_zmalloc_threadlocal(void);
.github/workflows/ci.yml:255
- This temporary filter replaces the full regression suite with five tests, leaving unrelated shutdown and allocation lifetimes unvalidated across the build matrix. Remove
test-casesso the normal suite runs before merge.
# debugging fast-shutdown crash: only the tests that actually crashed in CI, so
# this step doesn't wait on the full ~700-test suite while iterating on a fix.
test-cases: "test_cascaded_partial_sync|test_partial_sync|test_partial_replication_on_same_source_master_with_replica_lsn_inc|test_bgsave_during_stable_sync|test_tls_replication"
.github/workflows/ci.yml:213
- This condition permanently disables the cluster-mode C++ test suite, so the new shutdown path is not validated in cluster mode. Restore the original condition before merging.
if: github.event_name != 'push' && false # debugging fast-shutdown crash: skip unit tests
.github/workflows/ci.yml:225
- This condition permanently disables the hashtag-locking cluster test suite, so the new shutdown path is not validated in this configuration. Restore the original condition before merging.
if: github.event_name != 'push' && false # debugging fast-shutdown crash: skip unit tests
.github/workflows/ci.yml:154
- This condition permanently disables the primary IoUring C++ test suite, so the allocator and shutdown rewrite can merge without unit-test coverage. Restore the original condition before merging.
if: github.event_name != 'push' && false # debugging fast-shutdown crash: skip unit tests
- Files reviewed: 39/39 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Signed-off-by: Kostas Kyrimis <kostas@dragonflydb.io>
There was a problem hiding this comment.
🔵 Needs a closer look
The shutdown allocator regressions and unsafe CI container privileges must be resolved before approval.
Review details
Suppressed comments (1)
.github/workflows/ci.yml:87
- Granting
SYS_ADMINto a PR-controlled build container that also bind-mounts the host root (/:/hostroot) breaks the runner's isolation; the new helper then explicitly remounts/proc/syswritable. Remove this capability and rely on the unprivileged core-file watcher (or isolate core collection in a trusted job).
options: --security-opt seccomp=unconfined --sysctl "net.ipv6.conf.all.disable_ipv6=0" --cap-add SYS_ADMIN
- Files reviewed: 39/39 changed files
- Comments generated: 0 new
- Review effort level: Balanced
When we are shutting down we do not need to walk every single object in the db and run its destructor, the OS will reclaim the heap memory anyway. To avoid wasting time on that a special flag is added. It releases the pointer instead, leaking memory, but it will be reclaimed soon by the OS.
Recently slow shutdown watchdog triggers have increased. That might be caused due to many different issues, but releasing values will buy more time in a shutdown situation especially for large data sets.