Skip to content

Commit f1da115

Browse files
authored
chore: bindings for max_total_wal_size and flush() (#6)
* chore: max_total_wal_size option * chore: explose flush method in the DB class * chore: flush all column families instead when not specifying * ci: add tests for the new bindings * chore: add a Makefile * fix: remove rocksdb.cur-size-all-mem-tables from tests, because it apparently behaves differently in different environments * fix: uv now requires a --clear option when running 'uv venv' to clear existing environments. See astral-sh/uv#17757
1 parent 8599456 commit f1da115

8 files changed

Lines changed: 185 additions & 3 deletions

File tree

.github/workflows/test.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ jobs:
9797
CPPFLAGS: -I${{ env.LIBROCKSDB_PATH }}/include
9898
LDFLAGS: -L${{ env.LIBROCKSDB_PATH }}/lib
9999
run: |
100-
uv venv
100+
uv venv --clear
101101
uv pip install --editable .[test]
102102
103103
- name: Run tests
@@ -131,7 +131,7 @@ jobs:
131131

132132
- name: Build python-rocksdb
133133
run: |
134-
uv venv
134+
uv venv --clear
135135
uv pip install --editable .[test]
136136
137137
- name: Run tests
@@ -164,7 +164,7 @@ jobs:
164164

165165
- name: Build python-rocksdb
166166
run: |
167-
uv venv
167+
uv venv --clear
168168
uv pip install --editable .[test]
169169
170170
- name: Run tests

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.PHONY: test
2+
3+
test:
4+
pip install -e .[test]
5+
pytest rocksdb/tests/

docs/api/options.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,21 @@ Options objects
713713
| *Type:* ``int``
714714
| *Default:* ``0``
715715
716+
.. py:attribute:: max_total_wal_size
717+
718+
Once write-ahead logs exceed this size, we will start forcing the flush of
719+
column families whose memtables are backed by the oldest live WAL file
720+
(i.e. the ones that are causing all the space amplification). If set to 0
721+
(default), we will dynamically choose the WAL size limit to be
722+
[sum of all write_buffer_size * max_write_buffer_number] * 4.
723+
This option takes effect only when there are more than one column family as
724+
otherwise the wal size is dictated by the write_buffer_size.
725+
726+
Dynamically changeable through SetDBOptions() API.
727+
728+
| *Type:* ``int``
729+
| *Default:* ``0``
730+
716731
.. py:attribute:: manifest_preallocation_size
717732
718733
Number of bytes to preallocate (via fallocate) the manifest

rocksdb/_rocksdb.pyx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,6 +1229,12 @@ cdef class Options(ColumnFamilyOptions):
12291229
def __set__(self, value):
12301230
self.opts.WAL_size_limit_MB = value
12311231

1232+
property max_total_wal_size:
1233+
def __get__(self):
1234+
return self.opts.max_total_wal_size
1235+
def __set__(self, value):
1236+
self.opts.max_total_wal_size = value
1237+
12321238
property manifest_preallocation_size:
12331239
def __get__(self):
12341240
return self.opts.manifest_preallocation_size
@@ -2002,6 +2008,51 @@ cdef class DB(object):
20022008
st = self.db.CompactRange(c_options, cf_handle, begin_ptr, end_ptr)
20032009
check_status(st)
20042010

2011+
def flush(self, wait=True, column_families=None):
2012+
"""
2013+
Flush memtable data for column families.
2014+
2015+
If atomic flush is not enabled, flushing multiple column families is
2016+
equivalent to calling flush for each column family individually.
2017+
If atomic flush is enabled, all specified column families will be
2018+
flushed atomically up to the latest sequence number at the time
2019+
when flush is requested.
2020+
2021+
Args:
2022+
wait (bool): If True (default), the flush will wait until the
2023+
flush is done.
2024+
column_families: Specifies which column families to flush:
2025+
- None (default): flushes ALL column families in the database
2026+
- A single ColumnFamilyHandle: flushes only that column family
2027+
- A list/tuple of ColumnFamilyHandle objects: flushes those column families
2028+
"""
2029+
cdef Status st
2030+
cdef options.FlushOptions flush_opts
2031+
cdef vector[db.ColumnFamilyHandle*] cf_handles
2032+
cdef _ColumnFamilyHandle handle
2033+
flush_opts.wait = wait
2034+
2035+
# Handle different input types for column_families
2036+
if column_families is None:
2037+
# Flush ALL column families
2038+
for handle in self.cf_handles:
2039+
cf_handles.push_back(handle.handle)
2040+
elif isinstance(column_families, ColumnFamilyHandle):
2041+
# Single column family
2042+
cf_handles.push_back((<ColumnFamilyHandle?>column_families).get_handle())
2043+
elif isinstance(column_families, (list, tuple)):
2044+
# Multiple column families
2045+
for cf in column_families:
2046+
if not isinstance(cf, ColumnFamilyHandle):
2047+
raise TypeError("All items in column_families must be ColumnFamilyHandle objects")
2048+
cf_handles.push_back((<ColumnFamilyHandle?>cf).get_handle())
2049+
else:
2050+
raise TypeError("column_families must be None, a ColumnFamilyHandle, or a list of ColumnFamilyHandle objects")
2051+
2052+
with nogil:
2053+
st = self.db.Flush(flush_opts, cf_handles)
2054+
check_status(st)
2055+
20052056
@staticmethod
20062057
def __parse_read_opts(
20072058
verify_checksums=False,

rocksdb/db.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ cdef extern from "rocksdb/db.h" namespace "rocksdb":
167167
const string& GetName() except+ nogil
168168
const options.Options& GetOptions(ColumnFamilyHandle*) except+ nogil
169169
Status Flush(const options.FlushOptions&, ColumnFamilyHandle*) except+ nogil
170+
Status Flush(const options.FlushOptions&, const vector[ColumnFamilyHandle*]&) except+ nogil
170171
Status DisableFileDeletions() except+ nogil
171172
Status EnableFileDeletions() except+ nogil
172173
Status Close() except+ nogil

rocksdb/options.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ cdef extern from "rocksdb/options.h" namespace "rocksdb":
8888
int table_cache_numshardbits
8989
uint64_t WAL_ttl_seconds
9090
uint64_t WAL_size_limit_MB
91+
uint64_t max_total_wal_size
9192
size_t manifest_preallocation_size
9293
cpp_bool allow_mmap_reads
9394
cpp_bool allow_mmap_writes

rocksdb/tests/test_db.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,3 +734,101 @@ def test_compact_range(self):
734734

735735
self.db.compact_range(column_family=self.cf_b)
736736

737+
def test_flush(self):
738+
# Check initial state
739+
initial_l0_files = int(self.db.get_property(b'rocksdb.num-files-at-level0'))
740+
741+
# Write some data
742+
self.db.put(b"a", b"1")
743+
self.db.put(b"b", b"2")
744+
745+
# Flush with default parameters (all column families, wait=True)
746+
self.db.flush()
747+
748+
# Verify flush created SST files at level 0
749+
final_l0_files = int(self.db.get_property(b'rocksdb.num-files-at-level0'))
750+
self.assertGreater(final_l0_files, initial_l0_files)
751+
752+
def test_flush_no_wait(self):
753+
# Write some data
754+
self.db.put(b"a", b"1")
755+
756+
# Flush without waiting - just verify it doesn't raise an exception
757+
self.db.flush(wait=False)
758+
759+
def test_flush_all_column_families(self):
760+
# Check initial state for each column family
761+
initial_default_l0 = int(self.db.get_property(b'rocksdb.num-files-at-level0'))
762+
initial_cf_a_l0 = int(self.db.get_property(b'rocksdb.num-files-at-level0', self.cf_a))
763+
initial_cf_b_l0 = int(self.db.get_property(b'rocksdb.num-files-at-level0', self.cf_b))
764+
765+
# Write to multiple column families
766+
self.db.put(b"default_key", b"default_value")
767+
self.db.put((self.cf_a, b"a_key"), b"a_value")
768+
self.db.put((self.cf_b, b"b_key"), b"b_value")
769+
770+
# Flush all column families (default behavior)
771+
self.db.flush()
772+
773+
# Verify all column families were flushed (SST files created)
774+
final_default_l0 = int(self.db.get_property(b'rocksdb.num-files-at-level0'))
775+
final_cf_a_l0 = int(self.db.get_property(b'rocksdb.num-files-at-level0', self.cf_a))
776+
final_cf_b_l0 = int(self.db.get_property(b'rocksdb.num-files-at-level0', self.cf_b))
777+
778+
self.assertGreater(final_default_l0, initial_default_l0)
779+
self.assertGreater(final_cf_a_l0, initial_cf_a_l0)
780+
self.assertGreater(final_cf_b_l0, initial_cf_b_l0)
781+
782+
def test_flush_single_column_family(self):
783+
# Check initial state
784+
initial_cf_a_l0 = int(self.db.get_property(b'rocksdb.num-files-at-level0', self.cf_a))
785+
initial_cf_b_l0 = int(self.db.get_property(b'rocksdb.num-files-at-level0', self.cf_b))
786+
787+
# Write to multiple column families
788+
self.db.put((self.cf_a, b"a_key"), b"a_value")
789+
self.db.put((self.cf_b, b"b_key"), b"b_value")
790+
791+
# Flush only cf_a
792+
self.db.flush(column_families=self.cf_a)
793+
794+
# Verify only cf_a was flushed (SST files created)
795+
final_cf_a_l0 = int(self.db.get_property(b'rocksdb.num-files-at-level0', self.cf_a))
796+
final_cf_b_l0 = int(self.db.get_property(b'rocksdb.num-files-at-level0', self.cf_b))
797+
798+
self.assertGreater(final_cf_a_l0, initial_cf_a_l0)
799+
self.assertEqual(final_cf_b_l0, initial_cf_b_l0) # cf_b should NOT be flushed
800+
801+
def test_flush_multiple_column_families(self):
802+
# Check initial state
803+
initial_cf_a_l0 = int(self.db.get_property(b'rocksdb.num-files-at-level0', self.cf_a))
804+
initial_cf_b_l0 = int(self.db.get_property(b'rocksdb.num-files-at-level0', self.cf_b))
805+
806+
# Write to multiple column families
807+
self.db.put((self.cf_a, b"a_key"), b"a_value")
808+
self.db.put((self.cf_b, b"b_key"), b"b_value")
809+
810+
# Flush both cf_a and cf_b
811+
self.db.flush(column_families=[self.cf_a, self.cf_b])
812+
813+
# Verify both were flushed (SST files created)
814+
final_cf_a_l0 = int(self.db.get_property(b'rocksdb.num-files-at-level0', self.cf_a))
815+
final_cf_b_l0 = int(self.db.get_property(b'rocksdb.num-files-at-level0', self.cf_b))
816+
817+
self.assertGreater(final_cf_a_l0, initial_cf_a_l0)
818+
self.assertGreater(final_cf_b_l0, initial_cf_b_l0)
819+
820+
# Verify both were flushed (SST files created)
821+
final_cf_a_l0 = int(self.db.get_property(b'rocksdb.num-files-at-level0', self.cf_a))
822+
final_cf_b_l0 = int(self.db.get_property(b'rocksdb.num-files-at-level0', self.cf_b))
823+
824+
self.assertGreater(final_cf_a_l0, initial_cf_a_l0)
825+
self.assertGreater(final_cf_b_l0, initial_cf_b_l0)
826+
827+
def test_flush_invalid_column_families(self):
828+
# Test that passing invalid type raises TypeError
829+
with self.assertRaises(TypeError):
830+
self.db.flush(column_families="invalid")
831+
832+
with self.assertRaises(TypeError):
833+
self.db.flush(column_families=["invalid", self.cf_a])
834+

rocksdb/tests/test_options.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,14 @@ def test_row_cache(self):
171171
self.assertIsNone(opts.row_cache)
172172
opts.row_cache = cache = rocksdb.LRUCache(2*1024*1024)
173173
self.assertEqual(cache, opts.row_cache)
174+
175+
def test_max_total_wal_size(self):
176+
opts = rocksdb.Options()
177+
# Default value is 0
178+
self.assertEqual(opts.max_total_wal_size, 0)
179+
# Set to a specific value and verify setter works
180+
opts.max_total_wal_size = 100 * 1024 * 1024 # 100 MB
181+
self.assertEqual(opts.max_total_wal_size, 100 * 1024 * 1024)
182+
# Set back to 0 (dynamic)
183+
opts.max_total_wal_size = 0
184+
self.assertEqual(opts.max_total_wal_size, 0)

0 commit comments

Comments
 (0)