[Bug] Cold start on memory-constrained hosts: get_delta_data_after_ts materializes the whole delta list into RAM and can hang/OOM
Summary
On a memory-constrained host (verified on ~1.8 GB RAM Alibaba Cloud ECS), cold-starting the server OOMs / hangs after the vectordb accumulates a large delta table (verified at ~2.8 GB of pending delta bytes). Root cause is not malformed records (that's tracked in #1892) but volume: get_delta_data_after_ts materializes every delta record into memory at once.
Reproduction
- Run the local vectordb backend until the LevelDB delta table grows large (our prod had ~2.8 GB pending delta).
- Restart the server on a host with limited RAM (~1.8 GB).
- Startup: cold start takes ~6.5 min and the process is at risk of OOM-kill / indefinite hang while
_recoverreplays deltas.
Root cause
store_manager.py get_delta_data_after_ts (at v0.4.20, line 265):
def get_delta_data_after_ts(self, ns_ts: int) -> List[DeltaRecord]:
delta_kv_list = self.storage.seek_to_end(str(ns_ts), StoreManager.DeltaTable)
delta_list = [DeltaRecord.from_bytes(data=data[1]) for data in delta_kv_list] # <-- materializes the WHOLE delta list
return delta_listseek_to_end returns an iterator, but the list-comprehension forces every DeltaRecord into RAM before _recover streams them into upsert_data. On a large delta table this is the same peak-memory spike whether or not individual records are valid. This is orthogonal to #1892: even with zero malformed records, a large delta table + low RAM OOMs during playback.
Proposed fix (lazily stream instead of materialize)
The fix is a lazily-yielding generator — keep only one DeltaRecord in memory at a time. This is backward-compatible because every downstream caller (_recover, get_delta_data_after_ts) consumes it via iteration / list(). We have been running this in production for 3+ days across 3 cold-start regression cycles without issue:
def get_delta_data_after_ts(self, ns_ts: int) -> Iterator[DeltaRecord]:
"""Lazily deserialize delta records so a large delta table can be replayed on
memory-constrained hosts without materializing the whole list."""
for _, bytes_data in self.storage.seek_to_end(str(ns_ts), StoreManager.DeltaTable):
yield DeltaRecord.from_bytes(data=bytes_data)Callers that need a list can wrap with list(...); native iteration gets the streaming benefit for free.
Environment
- openviking 0.4.19 (same code at 0.4.20 — verified the wheel line 265 unchanged)
- local vectordb backend
- 1.8 GB RAM ECS, ~2.8 GB delta table
- cold start ~6.5 min with the lazy patch; vulnerable to OOM/hang without it
Related
- #1892 (malformed-record robustness) — different axis; our fix targets volume/laziness, not record validity. A lazy generator doesn't fix #1892's corrupt-record crash, and #1892's fix doesn't address peak-memory. Both are worth landing.
Happy to open a PR with the lazy generator if maintainers agree this axis is worth supporting.
Source: volcengine/OpenViking