π Chain of Filters
AbstractChainOfFilters is the small engine that runs ordered pipelines inside the index. It accepts an immutable context object plus a mutable result carrier and iterates over a list of Filter<Context, Result> steps. Each step returns true to continue or false to short-circuit, which is how we cheaply exit when work is already finished (cache hits) or impossible (invalid plan).
βοΈ Mechanics
- The base class keeps the ordered
List<Filter<Context, Result>>and exposes a singlefilter(context, result)method that drives the loop. - Steps see the immutable context and may update the mutable result; they must return
falseonce they have produced a terminal outcome so the chain stops immediately. - Subclasses wrap the call to
filterso they can prepare state/result objects and handle cleanup. Example:SegmentSplitPipeline#runcloses the iterator in afinallyblock even when a step short-circuits. - Filters are regular classes that implement
Filter<Context, Result>and can be reused or composed in different orders when building a pipeline.
π οΈ Typical Usage
1) Define small, focused filters:
final class ValidateInput implements Filter<RequestCtx, ProcessingState> {
@Override
public boolean filter(final RequestCtx ctx, final ProcessingState state) {
if (!ctx.isValid()) {
state.fail("missing fields");
return false; // stop the chain early
}
return true;
}
}
2) Compose them into a pipeline subclass that owns setup/teardown:
final class ProcessingPipeline
extends AbstractChainOfFilters<RequestCtx, ProcessingState> {
ProcessingPipeline(
final List<Filter<RequestCtx, ProcessingState>> steps) {
super(List.copyOf(steps));
}
ProcessingState run(final RequestCtx ctx) {
final ProcessingState state = new ProcessingState();
filter(ctx, state); // short-circuit rules apply
return state;
}
}
π Where It Is Used
Segment Searcher Pipeline (read path)
SegmentSearcherPipeline (src/main/java/org/hestiastore/index/segment/SegmentSearcherPipeline.java) runs a three-step lookup when serving SegmentSearcher#get:
final SegmentSearcherPipeline<K, V> pipeline = new SegmentSearcherPipeline<>(
List.of(
new SegmentSearcherStepDeltaCache<>(valueTypeDescriptor), // stop if cached hit or tombstone
new SegmentSearcherStepBloomFilter<>(), // stop if bloom says βdefinitely notβ
new SegmentSearcherStepIndexFile<>())); // stop after sparse-index + on-disk read
pipeline.run(ctx, result);
return result.getValue();
- Short-circuits: cache hit/tombstone, Bloom filter negative, or sparse-index probe missing/false-positive correction.
- Context/result types:
SegmentSearcherContextcarries the key plus caches/searcher;SegmentSearcherResultholds the found value (ornullwhen absent).
Segment Split Pipeline (split/compaction path)
SegmentSplitPipeline (src/main/java/org/hestiastore/index/segment/SegmentSplitPipeline.java) orchestrates the per-segment split flow inside SegmentSplitter#split:
final SegmentSplitPipeline<K, V> pipeline = new SegmentSplitPipeline<>(
List.of(
new SegmentSplitStepValidateFeasibility<>(), // throws if plan is invalid
new SegmentSplitStepOpenIterator<>(), // opens streaming iterator
new SegmentSplitStepCreateLowerSegment<>(), // prepares target segment
new SegmentSplitStepFillLowerUntilTarget<>(), // writes first half
new SegmentSplitStepEnsureLowerNotEmpty<>(), // guardrail for tiny splits
new SegmentSplitStepReplaceIfNoRemaining<>(segmentReplacer), // compaction path, may stop here
new SegmentSplitStepWriteRemainingToCurrent<>() // split path, always stops after writing
));
final SegmentSplitterResult<K, V> result = pipeline.run(ctx);
- Short-circuits: compaction path (
SegmentSplitStepReplaceIfNoRemaining) replaces the original segment and stops; the final write step stops after producing the split result. - Context/result types:
SegmentSplitContextcarries the segment, version controller, plan, and IDs;SegmentSplitStateowns the iterator, lower-segment handle, and finalSegmentSplitterResult. - Safety:
SegmentSplitPipeline#runwrapsfilterso the opened iterator closes even when a step throws or short-circuits.
β Guidelines for Adding New Pipelines
- Keep steps single-purpose and side-effect aware; ensure they set a final result before returning
false. - Copy the incoming steps list (
List.copyOf(...)) to avoid accidental reordering at runtime. - When resources need cleanup, wrap the
filterinvocation in atry/finallyinside your subclass. - Pair unit tests with short-circuit cases; see
src/test/java/org/hestiastore/index/AbstractChainOfFiltersTest.javafor a minimal example.