Skip to content

Implement VM_AllocateExternalMemory - #4128

Open
bandalgomsu wants to merge 3 commits into
valkey-io:unstablefrom
bandalgomsu:feature/allocate-external-memory
Open

Implement VM_AllocateExternalMemory#4128
bandalgomsu wants to merge 3 commits into
valkey-io:unstablefrom
bandalgomsu:feature/allocate-external-memory

Conversation

@bandalgomsu

Copy link
Copy Markdown
Contributor

Summary

This PR adds module APIs for accounting memory that is not tracked by zmalloc, and includes that memory in used_memory / maxmemory bookkeeping.

New APIs:

  • ValkeyModule_AllocateExternalMemory(size_t bytes)
  • ValkeyModule_FreeExternalMemory(size_t bytes)

These APIs are intended to be used by modules from a command callback or from a locked thread-safe context.

Problem

Some module architectures, especially asynchronous indexing pipelines, can create memory pressure that the core does not currently see.

A concrete example is the Search module ingestion flow:

  • key data is captured during the mutation command
  • indexing happens later on a background thread
  • the eventual index insertion can amplify memory usage relative to the original key payload
  • if many ingestions are queued, the core can substantially underestimate real memory pressure until that later work executes

That gap degrades OOM detection and allows the process to run significantly past the configured maxmemory threshold before the core reacts.

This also applies to memory that modules allocate outside zmalloc, for example OS-backed allocations such as mmap regions used for huge-page-backed data structures.

Solution

This change introduces an explicit external-memory accounting path for modules.

Internally:

  • the core keeps a separate external-memory counter
  • zmalloc_used_memory() adds that counter to the allocator-tracked total
  • as a result, maxmemory/OOM checks, used_memory, and used_memory_peak all include module external memory

Closes : #3339

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e7ac15bc-95d8-4c39-92e0-21d9420a6e45

📥 Commits

Reviewing files that changed from the base of the PR and between 7960bdf and 9f50527.

📒 Files selected for processing (3)
  • src/server.c
  • tests/modules/infotest.c
  • tests/unit/moduleapi/infotest.tcl
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/server.c

📝 Walkthrough

Walkthrough

Adds external memory accounting in zmalloc, exposes it through new module APIs, reports it in the INFO Debug section, and adds unit and module integration tests.

Changes

External memory accounting

Layer / File(s) Summary
zmalloc external memory counter and APIs
src/zmalloc.c, src/zmalloc.h, src/unit/test_zmalloc.cpp
Adds used_memory_external, includes it in zmalloc_used_memory(), introduces read/increase/decrease APIs with overflow and underflow checks, declares them in the header, and tests successful and invalid updates.
Module API exposure of external memory functions
src/module.c, src/valkeymodule.h
Adds module wrappers, registers them with the core API, resolves them during module initialization, and returns module errors for invalid updates.
INFO reporting and module integration coverage
src/server.c, tests/modules/infotest.c, tests/unit/moduleapi/infotest.tcl
Reports external memory in INFO Debug, adds a module command and unload cleanup hook, and verifies the field is excluded from INFO Memory.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ModuleTest
  participant ModuleAPI
  participant Zmalloc
  participant INFO
  ModuleTest->>ModuleAPI: set external memory
  ModuleAPI->>Zmalloc: update used_memory_external
  Zmalloc-->>ModuleAPI: return update status
  ModuleAPI-->>ModuleTest: return command result
  INFO->>Zmalloc: read external memory usage
  Zmalloc-->>INFO: return external usage
  INFO-->>ModuleTest: emit used_memory_module_external in Debug
Loading

Suggested reviewers: zuiderkwast

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: adding external memory accounting APIs.
Description check ✅ Passed The description clearly describes the new external-memory accounting APIs and their purpose.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/unit/test_zmalloc.cpp (1)

70-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add overflow/underflow error path tests.

The test covers the happy path well but doesn't validate the overflow/underflow guards in zmalloc_increase_used_memory_external and zmalloc_decrease_used_memory_external. Consider adding assertions for the -1 return on invalid updates.

♻️ Suggested additional test cases
 TEST_F(ZmallocTest, TestZmallocExternalUsedMemory) {
     size_t used_memory_before = zmalloc_used_memory();
     size_t external_memory_before = zmalloc_used_external_memory();

     ASSERT_EQ(zmalloc_increase_used_memory_external(123), 0);
     ASSERT_EQ(zmalloc_used_external_memory(), external_memory_before + 123);
     ASSERT_EQ(zmalloc_used_memory(), used_memory_before + 123);

     ASSERT_EQ(zmalloc_decrease_used_memory_external(123), 0);
     ASSERT_EQ(zmalloc_used_external_memory(), external_memory_before);
     ASSERT_EQ(zmalloc_used_memory(), used_memory_before);
+
+    /* Underflow: decreasing more than allocated should fail. */
+    ASSERT_EQ(zmalloc_decrease_used_memory_external(1), -1);
+
+    /* Overflow: increasing beyond SIZE_MAX should fail. */
+    ASSERT_EQ(zmalloc_increase_used_memory_external(SIZE_MAX), 0);
+    ASSERT_EQ(zmalloc_increase_used_memory_external(1), -1);
+    ASSERT_EQ(zmalloc_decrease_used_memory_external(SIZE_MAX), 0);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/unit/test_zmalloc.cpp` around lines 70 - 81, The Zmalloc external memory
test only covers successful updates and misses the guard paths in
zmalloc_increase_used_memory_external and zmalloc_decrease_used_memory_external.
Extend TestZmallocExternalUsedMemory in test_zmalloc.cpp with negative cases
that force overflow/underflow or invalid updates and assert the functions return
-1, while also verifying zmalloc_used_memory and zmalloc_used_external_memory
remain unchanged after those failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/zmalloc.c`:
- Around line 117-118: `used_memory_external` is a shared counter with
concurrent reads and writes, so it needs the same atomic handling used by
`used_memory_for_additional_threads`. Update the declaration in `src/zmalloc.c`
to use an atomic size type and modify the read/write paths in
`zmalloc_used_memory()` and the module callback update logic to use relaxed
atomic load/store or fetch-add operations, keeping `moduleGIL` as the writer
serialization but removing the C data race.

---

Nitpick comments:
In `@src/unit/test_zmalloc.cpp`:
- Around line 70-81: The Zmalloc external memory test only covers successful
updates and misses the guard paths in zmalloc_increase_used_memory_external and
zmalloc_decrease_used_memory_external. Extend TestZmallocExternalUsedMemory in
test_zmalloc.cpp with negative cases that force overflow/underflow or invalid
updates and assert the functions return -1, while also verifying
zmalloc_used_memory and zmalloc_used_external_memory remain unchanged after
those failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 094e030e-1ad3-444e-83e2-470588de8fe3

📥 Commits

Reviewing files that changed from the base of the PR and between 301b6cd and cf84948.

📒 Files selected for processing (6)
  • src/module.c
  • src/server.c
  • src/unit/test_zmalloc.cpp
  • src/valkeymodule.h
  • src/zmalloc.c
  • src/zmalloc.h

Comment thread src/zmalloc.c Outdated
Signed-off-by: Su Ko <rhtn1128@gmail.com>
@bandalgomsu
bandalgomsu force-pushed the feature/allocate-external-memory branch from cf84948 to eab7d7a Compare July 9, 2026 11:08
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.54545% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.80%. Comparing base (301b6cd) to head (9f50527).
⚠️ Report is 41 commits behind head on unstable.

Files with missing lines Patch % Lines
src/module.c 0.00% 8 Missing ⚠️
src/unit/test_zmalloc.cpp 94.44% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #4128      +/-   ##
============================================
+ Coverage     76.76%   76.80%   +0.03%     
============================================
  Files           162      162              
  Lines         81169    81525     +356     
============================================
+ Hits          62313    62618     +305     
- Misses        18856    18907      +51     
Files with missing lines Coverage Δ
src/server.c 89.51% <100.00%> (+<0.01%) ⬆️
src/zmalloc.c 86.47% <100.00%> (+0.87%) ⬆️
src/unit/test_zmalloc.cpp 98.18% <94.44%> (-1.82%) ⬇️
src/module.c 25.40% <0.00%> (-0.02%) ⬇️

... and 26 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@valkey-review-bot valkey-review-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including module-managed external memory in used_memory/maxmemory makes sense, but one classification path now reports those bytes as dataset memory as well.

Comment thread src/server.c Outdated
info,
"# Memory\r\n" FMTARGS(
"used_memory:%zu\r\n", zmalloc_used,
"used_memory_module_external:%zu\r\n", module_external_memory,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a dedicated used_memory_module_external field here makes the new category visible, but getMemoryOverheadData() still derives mh->dataset from zmalloc_used_memory() and subtracts only the built-in overhead buckets (src/object.c:1403, src/object.c:1499-1508). Because zmalloc_used_memory() now includes used_memory_external, the same bytes also flow into used_memory_dataset, MEMORY STATS dataset.bytes, and keys.bytes-per-key (src/object.c:1895-1902).

That reclassifies module-only external memory as dataset memory: a module that accounts background-queue or mmap-backed state here will make the dataset counters grow even when no additional keys were stored. Keep the counter in the total/maxmemory path, but thread it through serverMemOverhead as its own bucket (or subtract zmalloc_used_external_memory() before computing mh->dataset) so the dataset metrics keep describing keyspace memory.

Comment thread src/module.c Outdated
Comment on lines +608 to +618
/* Track memory that a module allocated outside the server allocator.
* The caller must invoke this API from a command callback or while holding
* a thread safe context lock. */
int VM_AllocateExternalMemory(size_t bytes) {
if (zmalloc_increase_used_memory_external(bytes) != 0) {
errno = ERANGE;
return VALKEYMODULE_ERR;
}
return VALKEYMODULE_OK;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why having the locking constraint? Ultimately this devolves to an atomic increment, so why have the restriction?

If we decide to retain the restriction, then we should check & abort when it's violated.

Comment thread src/zmalloc.c Outdated
Comment on lines +540 to +551
int zmalloc_increase_used_memory_external(size_t size) {
size_t current = atomic_load_explicit(&used_memory_external, memory_order_relaxed);
if (SIZE_MAX - current < size) return -1;
atomic_store_explicit(&used_memory_external, current + size, memory_order_relaxed);
return 0;
}

int zmalloc_decrease_used_memory_external(size_t size) {
size_t current = atomic_load_explicit(&used_memory_external, memory_order_relaxed);
if (current < size) return -1;
atomic_store_explicit(&used_memory_external, current - size, memory_order_relaxed);
return 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the "failure" when the sizes go out of bounds. But it looks like this is the only place that actually requires external mutual exclusion -- so it could be recoded with a compare & swap to provide equivalent functionality but remove the single-thread/external locking constraint.

Signed-off-by: Su Ko <rhtn1128@gmail.com>

@allenss-amazon allenss-amazon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me!

Comment thread src/server.c Outdated
info,
"# Memory\r\n" FMTARGS(
"used_memory:%zu\r\n", zmalloc_used,
"used_memory_module_external:%zu\r\n", module_external_memory,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we anticipate users needing this? Modules themselves can report their own memory usage

@allenss-amazon allenss-amazon Jul 21, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For developers, having visibility into this field could be useful. I don't see it being particularly helpful to end-users.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think reporting at both the core and module levels would be useful for developer

The core could report the total amount of external memory, while each module could report how much external memory it uses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. This seems like a good use case for it 👍

@allenss-amazon allenss-amazon Jul 23, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the INFO field issue is resolved now, right? Is this now in a state for voting?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code still has it in memory stats, right? @bandalgomsu are we aligned to move it to DEBUG?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I moved it to DEBUG in the latest commit.

@murphyjacob4

Copy link
Copy Markdown
Contributor

Once we decide on the INFO field, we can do a vote on the major decision

@murphyjacob4 murphyjacob4 added the major-decision-pending Major decision pending by TSC team label Jul 20, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/server.c`:
- Around line 6830-6835: Add an INFO integration regression test in the existing
tests/INFO coverage that verifies used_memory_module_external appears in INFO
DEBUG with the expected accounted value and is absent from the general INFO
MEMORY output. Reuse the existing test setup and INFO parsing/assertion helpers
rather than adding new infrastructure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9c2efe0c-442d-48dc-839d-a60e0ac587e6

📥 Commits

Reviewing files that changed from the base of the PR and between 46f845d and 7960bdf.

📒 Files selected for processing (1)
  • src/server.c

Comment thread src/server.c
Signed-off-by: Su Ko <rhtn1128@gmail.com>
@bandalgomsu
bandalgomsu force-pushed the feature/allocate-external-memory branch from 7960bdf to 9f50527 Compare July 26, 2026 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

major-decision-pending Major decision pending by TSC team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[NEW] Memory Reservation

3 participants