Skip to content

fix(database): apply the database's column family options to late-created column families - #721

Open
Minh-Ng wants to merge 3 commits into
HarperFast:mainfrom
Minh-Ng:fix/column-family-options
Open

fix(database): apply the database's column family options to late-created column families#721
Minh-Ng wants to merge 3 commits into
HarperFast:mainfrom
Minh-Ng:fix/column-family-options

Conversation

@Minh-Ng

@Minh-Ng Minh-Ng commented Jul 25, 2026

Copy link
Copy Markdown

Summary

A named column family does not exist when a database is first created, so it is added immediately
after DB::Open. That path built its own options instead of reusing the ones the database was
opened with, so RocksDB's defaults applied. Values passed to open() were silently dropped for
those column families, as were this library's own tuned defaults:

  • write buffer: 64MB, against a library default of 16MB or whatever was passed
  • max write buffer number: 2 against 16
  • max write buffer size to maintain: 0 against the derived value, leaving no memtable history for
    optimistic transaction conflict detection
  • noBlockCache: ignored

Databases using only the default column family are unaffected. Stored data is unaffected either
way; these govern how memtables are sized and flushed, not what gets written.

The descriptor now saves the column family options used by DB::Open. When a handle creates a
column family later, it starts with those saved options, then applies writeBufferSize,
maxWriteBufferNumber, and maxWriteBufferSizeToMaintain from that handle.

Named column families now use the configured write buffer, 16MB by default rather than 64MB, so
they will flush more frequently than they do today.

Test Plan

Added a test that opens two named column families, identical apart from write buffer size (64KB and
64MB), and writes 512KB to each. The smaller one flushes to disk unprompted; the larger one does
not. Both measured zero before this change, so the test fails without the fix.

Added a test that gives the default column family a 64MB write buffer and a named column family a
64KB write buffer. After writing 512KB to the named column family, the test confirms that it flushes
instead of inheriting the default column family's 64MB setting.

noBlockCache was confirmed manually: a named column family now reports no block cache, against
557KB of usage when caching is enabled. The remaining two options are corrected by the same change
but have no direct assertion, as I could not find a way to observe them through the JavaScript API.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request ensures that dynamically created column families inherit the same ColumnFamilyOptions (such as writeBufferSize) that the database was originally opened with, rather than falling back to RocksDB defaults. This is achieved by storing cfOptions in DBDescriptor and passing it to createRocksDBColumnFamily. A corresponding integration test has been added to verify this behavior. I have no feedback to provide.

@Minh-Ng
Minh-Ng force-pushed the fix/column-family-options branch 2 times, most recently from d9dd882 to ee88116 Compare July 25, 2026 01:33
@Minh-Ng
Minh-Ng marked this pull request as ready for review July 25, 2026 04:00

@kriszyp kriszyp 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.

Nice, good catch.


Additional inline comments:

  • test/db-options.test.ts:80: The production change also routes a missing CF through the already-open descriptor in DBRegistry::OpenDB, but these two fresh paths only exercise creation inside DBDescriptor::open. Could we add a same-path case that opens the default CF first with a 64 KiB buffer, then opens a missing named CF and verifies that it flushes? That would protect the retained cfOptions member and the registry wiring, which this test can currently pass without.

Comment thread test/db-options.test.ts

// A named CF is created after DB::Open, so it misses the open options.
// Observed via flush behavior: RocksDB exposes no property for the size.
it('should apply writeBufferSize to a named column family', () =>

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.

The production change also routes a missing CF through the already-open descriptor in DBRegistry::OpenDB, but these two fresh paths only exercise creation inside DBDescriptor::open. Could we add a same-path case that opens the default CF first with a 64 KiB buffer, then opens a missing named CF and verifies that it flushes? That would protect the retained cfOptions member and the registry wiring, which this test can currently pass without.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Added test coverage. Used a 64 MiB buffer for the first handle and 64 KiB for the late named CF because these memory settings are per-CF and the late handle’s value should win. The test verifies that the late CF automatically flushes through the DBRegistry::OpenDB path.

Comment thread src/binding/database/db_registry.cpp Outdated
DEBUG_LOG("%p DBRegistry::OpenDB Creating column family \"%s\"\n", instance.get(), name.c_str());
auto column = rocksdb_js::createRocksDBColumnFamily(entry.descriptor->db, name);
auto column = rocksdb_js::createRocksDBColumnFamily(
entry.descriptor->db, name, entry.descriptor->cfOptions

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.

Low: the already-open-DB late-CF path is untested

The new test opens two separate databases (each via generateDBPath()), so it only exercises DBDescriptor::open's late-CF branch. This branch — adding a named CF to an already-open DB using the freshly-stored descriptor->cfOptions — is the round-trip unique to this fix (options captured at first DB::Open, reused on a later open() for a new CF name) and has no coverage.

Worth a targeted test: open a DB with writeBufferSize: 64 * 1024, then open a second named CF on the same path, and assert it flushes (rocksdb.total-sst-files-size > 0). Note this path also means a writeBufferSize / noBlockCache passed on the later open call is intentionally dropped in favor of the first open's options — consistent with the PR's stated design, but a test would pin that behavior down.


Generated by Barber AI

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Covered by the new test discussed above

Comment thread test/db-options.test.ts Outdated
}

// Flushes run in the background.
const deadline = Date.now() + 5000;

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.

Would it be possible to force a await db.flush() and reduce the amount of time we need to wait? 5 seconds seems arbitrary, so I wonder if we could get this down to 1 second?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yes 5 seconds is arbitrary, agreed polling is not ideal. Calling flush() would remove the behavior the test is checking. Instead, we limit the CF to two write buffers. When both are full, RocksDB pauses later writes until it automatically flushes one. Since the test awaits every put(), it cannot reach the assertion until that flush completes, so no separate timeout or polling is needed.

Minh-Ng added 2 commits July 29, 2026 12:40
…ated CFs

Column families that don't exist when `DB::Open` runs are created afterwards
via `createRocksDBColumnFamily`, which built its own `ColumnFamilyOptions`
rather than reusing the database's. Every tunable the database configured
silently reverted to the RocksDB default for those families, which is every
named column family, since a named CF never exists on a fresh path.

Affected:
  - writeBufferSize: 64MB (RocksDB default) instead of the configured 16MB
  - maxWriteBufferNumber: 2 instead of 16
  - maxWriteBufferSizeToMaintain: 0 instead of the derived value, leaving no
    retained memtable history for OptimisticTransactionDB conflict checking
  - noBlockCache: ignored, so a no-cache database still cached blocks for its
    named column families

The descriptor now retains the options `DB::Open` was given and
`createRocksDBColumnFamily` takes them, so a future option propagates by
construction instead of needing a second edit here. The blob settings were
already duplicated across both sites, which is how the drift went unnoticed.

Behavior change: existing named column families move from 64MB to 16MB
memtables, which changes flush cadence.
@Minh-Ng
Minh-Ng force-pushed the fix/column-family-options branch from ee88116 to f2698db Compare July 29, 2026 21:18

@kriszyp kriszyp 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.

Maybe an improvement could be made to centralizing construction of ColumnFamilyOptions? Otherwise LGTM.
🤖 Reviewed with Codex

Comment thread src/binding/database/db_registry.cpp Outdated
auto column = rocksdb_js::createRocksDBColumnFamily(entry.descriptor->db, name);
// Preserve the retained table/blob settings, but apply the current
// handle's per-CF memory options to the family it is creating.
auto cfOptions = entry.descriptor->cfOptions;

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.

The follow-up correctly takes the three memtable fields from the handle creating this CF, but noBlockCache is also a per-CF setting: it is encoded in ColumnFamilyOptions::table_factory from that handle on the first-open path. Copying the descriptor's factory here makes the cache policy depend on whichever handle opened the path first. For example, opening the default family normally and then opening late with noBlockCache: true still lets a post-flush read populate the cache; reversing those options leaves late uncached even though it requested the default cache. Could we centralize construction of the complete ColumnFamilyOptions from the current DBOptions for both creation paths, or at least rebuild the table factory from options.noBlockCache here, and add a same-path regression with opposing cache policies?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Will centralize construction in buildColumnFamilyOptions() and use it from both creation path, thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants