#5064·OpenViking

[Bug] Cold start on memory-constrained hosts: get_delta_data_after_ts materializes the whole delta list into RAM and can hang/OOM

Author: livechenjt-progCreated Sep 15, 2026Updated Sep 17, 2026

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

  1. Run the local vectordb backend until the LevelDB delta table grows large (our prod had ~2.8 GB pending delta).
  2. Restart the server on a host with limited RAM (~1.8 GB).
  3. Startup: cold start takes ~6.5 min and the process is at risk of OOM-kill / indefinite hang while _recover replays deltas.

Root cause

store_manager.py get_delta_data_after_ts (at v0.4.20, line 265):

python
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_list

seek_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:

python
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.