Skip to content

Key-management review: six smaller findings (filter parameters, allocation bounds, concurrency, swallowed error) #91

Description

@PaulSnow

From a review of how the store manages keys once they leave the window. The serious one is #90; these are the rest, kept together so they are not lost.

1. Every filter runs K=3 at 12 bits/key, which is not the optimum

NewBloomSizedForKeys(count, 3) is used for segment indexes (indexmerge.go:192, segstore.go:3804), block sets (blockset.go:329), day filters (dayfilter.go:176) and key filters (segstore.go:929). Measured on real keys, 200k absent probes against a 120k-key filter:

filter pages/probe false positives
scattered, 12 bits, k=3 (today) 3 1.140%
blocked, 12 bits, k=3 1 1.071%
blocked, 12 bits, k=8 1 0.318%
blocked, 16 bits, k=8 1 0.060%

Blocking every key's bits into one page makes a probe one page touch however many hash functions it uses, which in turn makes the hash count free, and the optimum is about 0.7 x bits per key. K=3 exists because the current layout scatters bits and each hash function costs a page. Cost of the change: it alters the bit layout in every .idx and .bset, so a format version bump and a rebuild. Measurements are in database/keytable_sim_test.go (TestKeyTableBloomSim).

2. The day filter allocates the bitmap its bound exists to prevent

dayfilter.go:176:

bloom := NewBloomSizedForKeys(keys, 3)                 // allocates keys*12/8
if DayFilterMaxBytes > 0 && bloom.NumBytes > DayFilterMaxBytes {
    bloom = newBloomSized(DayFilterMaxBytes*8/BloomBitsPerKey, ...)
}

The first line already allocates the ~648 MB the comment at dayfilter.go:70 says must never be held, on the pack path with every shard pinned, before the cap is applied. Compute the size, clamp, then allocate.

3. SetStore.build has no such bound at all

blockset.go:329 sizes its bloom from the summed key count with no cap, and holds bloom.Map live from there until blockset.go:402. A first pack over a long backlog hits exactly the spec 1.2 problem DayFilterMaxBytes was written to avoid.

4. SetStore is not independently safe against concurrent builds

closeFinishedGroups snapshots a group's sets under the mutex (blockset.go:476), releases it, builds the filter, then re-reads state to publish. If a set could join that group in the gap, the committed filter would definitively deny that set's keys. In-process this is closed by KVShard.packMu, whose comment names exactly this failure — but the guard lives in KVShard, not in SetStore, and SetStore.build's own overlap check at blockset.go:294 reads Newest() outside the mutex it takes at :442. There is also no directory or process lock anywhere in the package, so two processes over one database directory would defeat both packMu and the PID-qualified tmp name.

5. SetGroupBlocks is validated only at open

openDayFilter refuses a filter built under a different group size (dayfilter.go:275), which is right, but it runs once at open and the in-memory dayFilter does not retain the size it was built under. If the exported var changes after OpenSetStore, setGroup computes group numbers under the new size while findDayFilter matches on the bare integer, so a filter built over one block range can be consulted for another and answer a confident "not here".

6. ImportSegmentFile reports success after dropping the filters

segstore.go:3581: after break on an addSegmentKeys error, return s.writeManifest() overwrites the named err. The filters were dropped, which is the safe direction, but the caller is told the import succeeded and never learns the store is now walking.

Also worth knowing

ForEach holds a dedup set proportional to distinct keys (iterate.go:20), so export and consistency checks cannot run at the billion-key scale the design now targets. The comment says so and names the fix: the k-way merge in indexmerge.go already emits keys in order with no such set.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions