Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Probabilistic & System-Scale Structures  ›  Lesson

Capstone: Quarry's Storage Engine

55 minWire the LRU cache, the LSM write/flush/compaction pipeline and its Bloom-filtered read path into one engine, and measure what a fixed workload actually costs it
You're building a piece ofQuarry — the storage engine
This piece — run_storage_engine(): Runs a fixed read/write mix through the whole engine and reports write amplification, cache hit rate and read path length.
Scenario Quarry's engineers want one report -- write amplification, cache hit rate, read path length -- before they ship this exact combination of cache size, memtable size and compaction threshold to production.
Your task
Build run_storage_engine(ops, memtable_limit, compaction_threshold, cache_capacity, m, k). ops is a fixed, seeded sequence of ('write', key, value) and ('read', key) operations. Writes go through an LSM pipeline (memtable -> flush -> cascading compaction) and write-through an LRU cache; reads check the cache first, and on a miss fall through to a Bloom-filtered probe of the LSM levels. Return {'write_amplification': ..., 'hit_rate': ..., 'total_probes': ...}. Example: run_storage_engine(generate_workload(8, 40, seed=3), 3, 2, 4, 32, 3) -> {'write_amplification': 1.38, 'hit_rate': 0.34, 'total_probes': 6}.

Capstone: Quarry's Storage Engine

In short: Quarry's storage engine is every piece from this section wired together. Writes land in a memtable, flush to an sstable, and cascade through compaction exactly like lesson 5 — and every write also write-through-updates an LRU cache from lesson 3. Reads check that cache first, for free; only a miss ever touches the LSM levels, and even then a Bloom filter (section 8) skips any sstable that provably doesn't hold the key. Running a fixed, seeded mix of reads and writes through the whole thing gives three numbers that actually matter in production: write amplification (what writes cost later), hit rate (how often the cache saves a disk probe at all), and read path length (how many probes a miss costs when it does happen). None of those three numbers can be reasoned about separately — they trade against each other through memtable_limit, compaction_threshold and cache_capacity, and the only way to see the trade is to run it.

The three pieces in one loop: a write arrow into memtable/LSM, a read arrow first into the LRU cache, a dashed arrow from a cache miss down into the LSM's Bloom-filtered probe, and three result tiles — write amplification, hit rate, read path length

Wire the cache in front of the LSM levels

A write touches both halves of the engine; a read only touches the LSM half when the cache has nothing for it:

step through it
1def cache_touch(order, cache_capacity, key):
2 new_order = [key] + [x for x in order if x != key]
3 if len(new_order) > cache_capacity:
4 new_order.pop()
5 return new_order
6 
7def tiny_engine(ops, cache_capacity):
8 cache_order, cache_values = [], {}
9 hits = misses = 0
10 for op in ops:
11 if op[0] == "write":
12 _, key, value = op
13 cache_values[key] = value
14 cache_order = cache_touch(cache_order, cache_capacity, key)
15 print(f"write {key}: cache is now {cache_order}")
16 else:
17 _, key = op
18 if key in cache_values:
19 hits += 1
20 cache_order = cache_touch(cache_order, cache_capacity, key)
21 print(f"read {key}: HIT, cache is now {cache_order}")
22 else:
23 misses += 1
24 print(f"read {key}: MISS -- would fall through to the LSM probe")
25 print(f"hits={hits} misses={misses}")
26 
27tiny_engine([("write", "a", 1), ("read", "a"), ("read", "b"),
28 ("write", "b", 2), ("read", "a"), ("read", "c")], cache_capacity=2)

a is a hit on its second read — the write-through cache already had it. b is a miss the first time (nothing's been written yet), then a hit once it's written. c is a miss from start to finish: it was never written in this trace, so there's nothing anywhere to find. Only the misses would ever reach probe_count_bloom in the real engine; every hit is answered entirely out of the cache dict, no disk probe at all.

Run a fixed workload and read the three numbers off it

A hand-written trace can't show what a realistic mix of reads and writes does at scale. generate_workload builds a reproducible one, and run_storage_engine reports the three numbers that matter:

python

40 operations over 8 keys: write amplification of 1.38 (every byte written costs 1.38 bytes of real disk I/O once compaction is counted), a 34% cache hit rate, and only 6 total disk probes across every miss — most misses find their key in the first sstable the Bloom filter lets through, or get ruled out before a probe happens at all.

Quick practice

What does this print?
cache_values = {"a": 1, "b": 2}
cache_order = ["b", "a"]
key = "c"
hit = key in cache_values
print(hit)
What does this print?
def cache_touch(order, cache_capacity, key):
    new_order = [key] + [x for x in order if x != key]
    if len(new_order) > cache_capacity:
        new_order.pop()
    return new_order

order = []
for key in ["a", "b", "c"]:
    order = cache_touch(order, 2, key)
print(order)

🎯 Your turn

Build run_storage_engine(ops, memtable_limit, compaction_threshold, cache_capacity, m, k): replay a fixed, seeded operation mix through the whole engine and report write amplification, cache hit rate and total read probes.

  • run_storage_engine(generate_workload(8, 40, seed=3), 3, 2, 4, 32, 3) → {'write_amplification': 1.38, 'hit_rate': 0.34, 'total_probes': 6}

Hint: flush_and_compact is one write's worth of lesson 5's run_lsm pipeline — call it after every write. A write also write-through-updates the cache with cache_touch. A read checks the cache dict first (0 probes on a hit); on a miss, probe_count_bloom finds it in the LSM levels, and the key then joins the cache too.

Press ▶ Run, try the Live App chips, then ✓ Check.

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upA fixed, seeded read/write mix

Write generate_workload(num_keys, num_ops, seed): using random.Random(seed), build num_ops operations over keys 'k0'..'k{num_keys-1}'. Each op picks a random key; with probability 0.3 it's ('write', key, f'v{rng.randint(0, 999)}'), otherwise ('read', key). len(generate_workload(3, 5, 1)) -> 5.

probability
DrillTouch the LRU cache's order

Write cache_touch(order, cache_capacity, key): move key to the front of order (inserting it if new), then trim the LRU (last) end if the list now exceeds cache_capacity. cache_touch(['b', 'a'], 2, 'c') -> ['c', 'b'].

cache-eviction
BuildOne write's worth of the LSM pipeline

Write flush_and_compact(memtable, levels, memtable_limit, compaction_threshold): if memtable has reached memtable_limit entries, flush it to a new sstable in levels[0] and cascade-compact any level that has reached compaction_threshold sstables, exactly like run_lsm (lesson 5) does per write. Return (memtable, levels, bytes_written) -- the possibly-updated state plus bytes written THIS call. flush_and_compact({'a': 1}, [], 3, 2) -> ({'a': 1}, [], 0).

distributed-structures
BossThe Bloom-filtered read path, again

Write probe_count_bloom(key, memtable, levels, m, k), identical to lesson 5's: 0 if key is in memtable; otherwise scan sstables newest-first, skipping any whose m-bit, k-hash Bloom filter rules key out, and stop counting the moment a scan finds it. probe_count_bloom('ghost', {}, [], 32, 3) -> 0.

probabilistic-structuresbit-manipulation
CapstoneHow long is a read path, on average?

Write average_read_path(ops, memtable_limit, compaction_threshold, cache_capacity, m, k): run run_storage_engine and return total_probes divided by the number of 'read' ops, rounded to 2 decimals (a cache hit counts as 0 probes in that average). average_read_path([('read','a')], 2, 2, 2, 32, 3) -> 0.0.

distributed-structurescache-policies
run_storage_engine.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
run_storage_engine(ops, memtable_limit, compaction_threshold, cache_capacity, m, k) → dictWrite amplification, cache hit rate and total Bloom-filtered read probes for a fixed read/write operation mix.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.