Skip to content

Cpp code dump - #1

Merged
eddyxu merged 8 commits into
mainfrom
lei/init_cpp
Jul 8, 2022
Merged

Cpp code dump#1
eddyxu merged 8 commits into
mainfrom
lei/init_cpp

Conversation

@eddyxu

@eddyxu eddyxu commented Jul 8, 2022

Copy link
Copy Markdown
Member

The initial patch to open source C++ codebase.

@eddyxu
eddyxu requested a review from changhiskhan July 8, 2022 06:08
@eddyxu
eddyxu merged commit 13f3e27 into main Jul 8, 2022
@eddyxu
eddyxu deleted the lei/init_cpp branch July 8, 2022 06:16
westonpace pushed a commit that referenced this pull request Jul 25, 2024
before:
<img width="710" alt="Screenshot 2024-07-21 at 4 24 44 PM"
src="https://github.com/user-attachments/assets/65a953ac-bbbf-4244-b3fa-d5b7c368806e">

after:
<img width="589" alt="Screenshot 2024-07-21 at 4 28 49 PM"
src="https://github.com/user-attachments/assets/16a20200-d8f2-4e5a-8e59-2be42281222e">


to reproduce:
`cargo run --release --example benchmark `
in `rust/lance-encoding/compression-algo/fsst`

machine info:
`11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz`
`Linux 192 5.10.0-28-amd64 #1 SMP Debian 5.10.209-2 (2024-01-31) x86_64
GNU/Linux`
westonpace pushed a commit that referenced this pull request May 14, 2026
…es (#6767)

## Summary

After a writer flushed a memtable to L0 and an external compactor merged
that generation into the base table — legitimately draining
`flushed_generations` to empty — a subsequent restart re-replayed the
original WAL entries into the new active memtable, duplicating rows on
read.

Two bugs were interacting:

1. **Disambiguation:** `replay_memtable_from_wal` distinguished "fresh
shard" from "flushed and compacted" via
`flushed_generations.is_empty()`. That works in a closed-world
deployment but breaks the moment an external compactor enters the
picture — and the compactor is the *intended* consumer that drains that
vector, so the signal is structurally broken under OSS-WAL.

2. **Cursor never advanced:** `MemTableFlusher::flush` read
`covered_wal_entry_position` from
`memtable.last_flushed_wal_entry_position()`, but that field is only set
by the `mark_wal_flushed` test helper. In production it stayed at 0, so
`replay_after_wal_entry_position` never advanced past 0. Under 0-based
WAL positions this masked bug #1 — both "fresh" and "post-flush-of-0"
produced cursor=0.

## Fix

- **WAL positions are now 1-based** (`FIRST_WAL_ENTRY_POSITION = 1`). A
cursor of `0` unambiguously means "no flush has stamped this shard," so
replay collapses to `cursor.saturating_add(1)` without consulting
`flushed_generations`.
- **`WalFlushHandler::handle`** writes the just-appended position back
into `state.last_flushed_wal_entry_position` under the state lock before
signalling the completion cell.
- **`MemTableFlusher::flush` / `flush_with_indexes`** now take an
explicit `covered_wal_entry_position` arg. The production caller derives
it per-memtable from the `WalFlushResult` carried in the completion cell
— authoritative under concurrent flushes — falling back to
`memtable.frozen_at_wal_entry_position()` when freeze did not trigger a
flush.
- **State seed at open** uses the post-replay WAL tip, not
`manifest.wal_entry_position_last_seen` (the latter is bumped on every
tailer read and can sit above any flushed generation).
- Proto field docs on `ShardManifest.replay_after_wal_entry_position` /
`wal_entry_position_last_seen` updated to spell out the 1-based
convention and what default-0 means.

## Test plan

- [x] Added
`test_memtable_replay_skips_entries_after_external_compaction` in
`rust/lance/src/dataset/mem_wal/write.rs`: open writer, put rows, close
(flush), simulate the compactor by directly committing a manifest with
empty `flushed_generations`, reopen, assert the memtable is empty. Fails
on the pre-fix code; passes now.
- [x] `cargo test -p lance --lib dataset::mem_wal` — 236/236 pass
- [x] `cargo test -p lance --lib` — 1600/1600 pass
- [x] `cargo test -p lance-index --lib` — 302/302 pass
- [x] `cargo clippy --all --tests --benches -- -D warnings` — clean
- [x] `cargo fmt --all -- --check` — clean

## Compatibility

WAL position numbering changes from 0-based to 1-based. Existing on-disk
manifests / WAL files written by the prior `oss-wal-multiplex` code are
not migrated — coordinated with downstream consumers (sophon) to start
fresh.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
geruh referenced this pull request in geruh/lance May 16, 2026
…es (lance-format#6767)

## Summary

After a writer flushed a memtable to L0 and an external compactor merged
that generation into the base table — legitimately draining
`flushed_generations` to empty — a subsequent restart re-replayed the
original WAL entries into the new active memtable, duplicating rows on
read.

Two bugs were interacting:

1. **Disambiguation:** `replay_memtable_from_wal` distinguished "fresh
shard" from "flushed and compacted" via
`flushed_generations.is_empty()`. That works in a closed-world
deployment but breaks the moment an external compactor enters the
picture — and the compactor is the *intended* consumer that drains that
vector, so the signal is structurally broken under OSS-WAL.

2. **Cursor never advanced:** `MemTableFlusher::flush` read
`covered_wal_entry_position` from
`memtable.last_flushed_wal_entry_position()`, but that field is only set
by the `mark_wal_flushed` test helper. In production it stayed at 0, so
`replay_after_wal_entry_position` never advanced past 0. Under 0-based
WAL positions this masked bug #1 — both "fresh" and "post-flush-of-0"
produced cursor=0.

## Fix

- **WAL positions are now 1-based** (`FIRST_WAL_ENTRY_POSITION = 1`). A
cursor of `0` unambiguously means "no flush has stamped this shard," so
replay collapses to `cursor.saturating_add(1)` without consulting
`flushed_generations`.
- **`WalFlushHandler::handle`** writes the just-appended position back
into `state.last_flushed_wal_entry_position` under the state lock before
signalling the completion cell.
- **`MemTableFlusher::flush` / `flush_with_indexes`** now take an
explicit `covered_wal_entry_position` arg. The production caller derives
it per-memtable from the `WalFlushResult` carried in the completion cell
— authoritative under concurrent flushes — falling back to
`memtable.frozen_at_wal_entry_position()` when freeze did not trigger a
flush.
- **State seed at open** uses the post-replay WAL tip, not
`manifest.wal_entry_position_last_seen` (the latter is bumped on every
tailer read and can sit above any flushed generation).
- Proto field docs on `ShardManifest.replay_after_wal_entry_position` /
`wal_entry_position_last_seen` updated to spell out the 1-based
convention and what default-0 means.

## Test plan

- [x] Added
`test_memtable_replay_skips_entries_after_external_compaction` in
`rust/lance/src/dataset/mem_wal/write.rs`: open writer, put rows, close
(flush), simulate the compactor by directly committing a manifest with
empty `flushed_generations`, reopen, assert the memtable is empty. Fails
on the pre-fix code; passes now.
- [x] `cargo test -p lance --lib dataset::mem_wal` — 236/236 pass
- [x] `cargo test -p lance --lib` — 1600/1600 pass
- [x] `cargo test -p lance-index --lib` — 302/302 pass
- [x] `cargo clippy --all --tests --benches -- -D warnings` — clean
- [x] `cargo fmt --all -- --check` — clean

## Compatibility

WAL position numbering changes from 0-based to 1-based. Existing on-disk
manifests / WAL files written by the prior `oss-wal-multiplex` code are
not migrated — coordinated with downstream consumers (sophon) to start
fresh.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@westonpace westonpace mentioned this pull request May 21, 2026
3 tasks
hamersaw referenced this pull request in hamersaw/lance Jun 4, 2026
…KeyIndex

Per jackye1995's review #1 ("composite-key BTreeMemIndex"): instead of a
separate parallel skiplist (`PkKeyIndex`), the composite PK index is a plain
`BTreeMemIndex` keyed on a synthetic `Binary` column (`__pk_key__`) holding the
order-preserving encoded tuple. `pk_key.rs` is now just the encoder
(`encode_pk_tuple` + `encode_pk_batch`); the insert path materializes the encoded
`Binary` column and feeds the existing index, and the probe seeks with
`ScalarValue::Binary(encode_pk_tuple(values))`.

Benefits: the composite case reuses `BTreeMemIndex`'s byte backend (incl. the
inline-small-key node optimization) and its `to_training_batches`, so the
in-memory probe, flush sidecar, and single-column path share one index type and
one code path. Arity-split and its single-column memory/typed-fast-path wins are
unchanged. Net deletion of the hand-rolled `PkKeyIndex` skiplist.

mem_wal suite green; clippy --tests -D warnings clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
westonpace added a commit that referenced this pull request Jul 8, 2026
…rvation (#7675)

Raises the per-partition memory pool from 100MB to 150MB and sets the
sort spill reservation to 40MB (up from the DataFusion default of 10MB)
to give sort operations more headroom while spilling to disk (we should
still spill at roughly the same rate).

The previous defaults were 100MB / 10MB. This _usually_ worked but
certain patterns would lead to false memory exhaustion errors:

```
OSError: LanceError(IO): Resources exhausted: Additional allocation failed for ExternalSorterMerge[0] with top memory consumers (across reservations) as:
  ExternalSorterMerge[0]#1(can spill: false) consumed 58.5 MB, peak 58.5 MB,
  ExternalSorter[0]#0(can spill: true) consumed 41.4 MB, peak 89.9 MB.
Error: Failed to allocate additional 345.9 KB for ExternalSorterMerge[0] with 27.6 MB already allocated for this reservation - 92.2 KB remain available for the total pool, /home/pace/lance/rust/lance-datafusion/src/chunker.rs:49:46
```

The problem happens as follows:

1. The sort node accumulates batches of data without modifying them
until it determines a spill is needed. During this phase each batch
counts double against the pool reservation. This is meant to provide
overhead for the later steps. In our above example we can see spilling
was triggered at 41.4MB which is about half of the 90MB pool (half,
because each batch is counted double)
2. The sort node determines that spilling is needed. First, it must sort
the data that has accumulated in memory. Each batch is sorted by itself.
This batch sort is in-place and doesn't affect reservations much.
3. A cursor is created for each in-memory batch. The in-memory batches
are then fed into a merge sort.
4. The merge sort accumulates batches of data to send to the spill. Once
a batch is accumulated it is written to the spill file.

Both the cursors and the accumulation require additional space. This is
the `ExternalSorterMerge[0]` mentioned above. It is given the
overcounting described in step 1. In other words, once this starts, we
have half the reservation in `ExternalSorter[0]` and half the
reservation in `ExternalSorterMerge[0]`. This "additional space"
_should_ be about the same size as the input. This is why we count each
batch twice.

In practice, the `ExternalSorterMerge[0]` reservation ends up being
slightly higher (for various reasons). This is what the
`sort_spill_reservation_bytes` is supposed to account for. Datafusion
defaults this to 10MB. There is no guidance (and I can't get Claude to
come up with any good guidance) as to what this value should be set to.
However, 10MB seems like too little. This PR updates it to about 1/3 of
the memory pool size. In theory it shouldn't grow proportionally to the
memory pool but in practice it seems to. I also really don't want to
expose it as yet another knob that users have to tune so I'm hoping 1/3
is slightly conservative but good enough.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
wombatu-kun referenced this pull request in wombatu-kun/lance Jul 30, 2026
…rvation (lance-format#7675)

Raises the per-partition memory pool from 100MB to 150MB and sets the
sort spill reservation to 40MB (up from the DataFusion default of 10MB)
to give sort operations more headroom while spilling to disk (we should
still spill at roughly the same rate).

The previous defaults were 100MB / 10MB. This _usually_ worked but
certain patterns would lead to false memory exhaustion errors:

```
OSError: LanceError(IO): Resources exhausted: Additional allocation failed for ExternalSorterMerge[0] with top memory consumers (across reservations) as:
  ExternalSorterMerge[0]#1(can spill: false) consumed 58.5 MB, peak 58.5 MB,
  ExternalSorter[0]#0(can spill: true) consumed 41.4 MB, peak 89.9 MB.
Error: Failed to allocate additional 345.9 KB for ExternalSorterMerge[0] with 27.6 MB already allocated for this reservation - 92.2 KB remain available for the total pool, /home/pace/lance/rust/lance-datafusion/src/chunker.rs:49:46
```

The problem happens as follows:

1. The sort node accumulates batches of data without modifying them
until it determines a spill is needed. During this phase each batch
counts double against the pool reservation. This is meant to provide
overhead for the later steps. In our above example we can see spilling
was triggered at 41.4MB which is about half of the 90MB pool (half,
because each batch is counted double)
2. The sort node determines that spilling is needed. First, it must sort
the data that has accumulated in memory. Each batch is sorted by itself.
This batch sort is in-place and doesn't affect reservations much.
3. A cursor is created for each in-memory batch. The in-memory batches
are then fed into a merge sort.
4. The merge sort accumulates batches of data to send to the spill. Once
a batch is accumulated it is written to the spill file.

Both the cursors and the accumulation require additional space. This is
the `ExternalSorterMerge[0]` mentioned above. It is given the
overcounting described in step 1. In other words, once this starts, we
have half the reservation in `ExternalSorter[0]` and half the
reservation in `ExternalSorterMerge[0]`. This "additional space"
_should_ be about the same size as the input. This is why we count each
batch twice.

In practice, the `ExternalSorterMerge[0]` reservation ends up being
slightly higher (for various reasons). This is what the
`sort_spill_reservation_bytes` is supposed to account for. Datafusion
defaults this to 10MB. There is no guidance (and I can't get Claude to
come up with any good guidance) as to what this value should be set to.
However, 10MB seems like too little. This PR updates it to about 1/3 of
the memory pool size. In theory it shouldn't grow proportionally to the
memory pool but in practice it seems to. I also really don't want to
expose it as yet another knob that users have to tune so I'm hoping 1/3
is slightly conservative but good enough.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 18381f3)
hamersaw added a commit that referenced this pull request Aug 26, 2026
…emory (#7831)

## Why

MemWAL's backpressure is a concrete struct whose only seam is a per-call
closure, and it can see one shard's **row** bytes. That leaves two gaps:

1. **An embedder cannot express its own budget.** A process running many
shards has limits lance cannot know — a process-wide memtable total, a
page-cache working set. Nothing could represent them, and nothing could
refuse a write: the valve only ever blocked, unboundedly.
2. **The bytes it measures are not the bytes that OOM you.**
`MemTable::estimated_size` counts buffered batches + the PK bloom
filter. Every in-memory index is invisible to it.

Gap 2 is not a rounding error. `HnswGraph::try_new` pre-allocates one
node per unit of `capacity` (= `max_memtable_rows`), so a vector
memtable commits its **entire** graph on the first insert:

| `max_memtable_rows` | HNSW on row #1 |
|---|---|
| 125k | 64.7 MiB |
| 500k | 258 MiB |
| 1M | 517 MiB |
| 2M | 1033 MiB |

Measured, not estimated, and identical at dim=768 and dim=8 — vectors
are held by reference, so the cost is ~542 B per row of *capacity*
regardless of dimension. A memtable holding one row costs the same as a
full one, while `estimated_size` reads ~zero.

## What

**Accounting** (`resident_bytes` at each layer, all cheap enough for the
write path):

- **HNSW** — the node arena and lookup slabs are sized from `capacity`
at construction, so the total is computed once in the loop `try_new`
already runs, plus an atomic for the rebuilt `packed_level0`.
- **BTree** — the skiplist arena counts chunks in its cold `grow` path:
free per insert, exact for the nodes.
- **FTS** — partitions are capped at `MAX_PARTITIONS` and size
themselves in O(1); only the mutable tail needed a running counter, kept
in step with the existing walk at its single growth point. A test
asserts the two agree exactly, including across a freeze.

Row bytes keep their data-only meaning — they size the flush unit, so a
generation stays a function of the rows in it. They move off
`MemTableStats` onto `ShardMemory::row_bytes()`, alongside
`index_bytes()`, `frozen_bytes()`, `grace_bytes()` and
`retained_bytes()`; `InMemoryMemTableRef::resident_bytes` is the
per-memtable total the ceiling is built on.

**The seam:**

```rust
async fn maybe_apply_backpressure(&self, shard: ShardMemory) -> Result<()>
```

`ShardWriterConfig::backpressure` **replaces** the built-in valve rather
than layering on it, so one implementation owns the whole policy. That
is why the gate receives a live view of what the calling shard holds: a
replacement can enforce a per-shard ceiling in the same place as its
process-wide one, without a back-reference into the writer calling it.
`ShardMemory` is deliberately live rather than a snapshot — a controller
that delays is waiting for those bytes to fall, so a captured copy would
never observe the drain.

The gate is deliberately **not** told the incoming batch's size. Batches
are decoded and resident by the time it runs, so there is nothing left
to reserve against — refusing does not un-allocate them, and bounding a
single write's memory belongs to the ingress. This is the settled shape
rather than a first cut: adding a parameter later would break every
external implementor.

With nothing injected, `LocalBackpressureController` keeps today's
per-shard behaviour, its two write modes covered by a `LocalSource` enum
instead of the closure.

**Counters.** `unflushed_memtable_bytes` read through a `try_read()`
that yields `0` whenever the write lock is held — and tokio's `RwLock`
is write-preferring, so also whenever a writer is merely *queued*. It
zeroed exactly the shards taking writes, reporting lowest under the
heaviest load. Replaced with `active_bytes()` / `frozen_bytes()`:
relaxed loads of counters maintained under the write lock, index memory
included.

**`Error::Backpressure`** (with `is_backpressure()`) makes a rejection
distinguishable from a real failure without matching on the message.

**Reservations are validated at `open()`.** An HNSW graph is charged
from `max_memtable_rows` before its first insert, while only row bytes
seal a memtable. A reservation with no room left under
`max_unflushed_memtable_bytes` would therefore put a shard over budget
at zero rows — nothing to seal, so nothing to flush, so every put stalls
and then fails as `Backpressure`, which is supposed to mean "retry
later" and never comes true. `open()` now rejects that configuration
outright, requiring `index_reserved + max_memtable_size <=
max_unflushed_memtable_bytes`, with both figures named in the error.
Only the built-in valve reads that ceiling, so the check is skipped when
a controller is injected.

## Update: the process-wide counter is gone

Earlier revisions carried `ShardWriterConfig::pod_memory_bytes`, an
`Arc<AtomicUsize>` every writer added to on insert and subtracted from
on flush-commit, so an embedder could read a process-wide total with one
load instead of scanning every shard.

**It has been removed**, because an incremental counter of that shape
has no way back. Nothing releases a writer's residual bytes when it goes
away without a final flush — `abort()` is `shutdown_all()` and nothing
else, and there is no `Drop` on `MemoryCounters` — so every teardown
that skips a flush leaks its memtable into the sum permanently. The
downstream embedder evicts that way on four separate paths, and enough
of them leave a process refusing every write against a memtable that
reads empty.

A `Drop` impl would close that particular hole. But the embedder is
better served recomputing the total from `memtable_stats()`, which is
also what it exports: derived, self-healing within one interval, and
identical to its own gauge by construction. So the counter is deleted
rather than patched, and the seam is smaller for it — net −41 lines.

`ShardMemory` and the per-shard `active`/`frozen` counters are
untouched. They are the live per-shard view a controller needs to
enforce a per-shard ceiling, and they have no cross-shard lifetime
problem.

## Contract changes

- *"Never errors due to backpressure"* → an **injected** controller may
reject. The built-in valve still never does.
- *"Never drops data"* → never drops **acked** data. A rejected write
was never accepted.
- `active`/`frozen` now count index memory, so the built-in valve is
meaningfully tighter on vector tables at a given
`max_unflushed_memtable_bytes`. That is the correct direction for a
memory valve, but it is a behaviour change worth knowing about.

## Follow-up (not this PR)

The graph costs ~5x the adjacency it stores: level-0 links are held
three times (`ranked` with distances, `published`, `packed_level0`), and
it is ~2-3 allocations per node. The reference implementation for the
fix already lives next door in `mem_wal/index/arena_skiplist.rs`, whose
BTree gets 4 MiB where HNSW spends 64.7 MiB. Filing separately — this PR
only makes the cost *visible*.

## Test

`cargo test -p lance --lib -- dataset::mem_wal`. New: HNSW
pre-allocation behaviour, FTS counter-vs-walk equality,
injected-replaces-default, default-when-none-injected,
reject-surfaces-as-Backpressure, and `ShardMemory` liveness across polls
(it hangs if the view ever regresses to a copy).

`cargo fmt --all` + `cargo clippy -p lance --lib --tests --benches`
clean in touched files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Xuanwo <github@xuanwo.io>
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.

1 participant