Extends DecayingBloomFilter to provide dual-buffered hash sets with automatic decay switching between active and previous buffers. Optimized for high-throughput scenarios with frequent insertions and periodic cleanup operations.
Memory usage analysis shows significant optimization potential:
- 32 bytes, peak 10 entries in 1m (320 on fast routers)
- 4 bytes, peak 150 entries in 10s (1600 on fast routers)
- 8 bytes, peak 1K entries in 2m (36K on fast routers)
- 16 bytes, peak 15K entries in 10m (15K on fast routers)
Performance characteristics:
- Zero false positive rate for ≤8 byte keys (lossy hash for larger keys)
- Over 8 bytes entries are reduced to a 64 bit hash, so a false positive needs a hash collision: about 5.4E-20 per pair, so n^2/2^65 across n entries
- About 1.93x faster than
DecayingBloomFilterfor 8 byte (long) entries and 2.46x faster for 16 byte entries - About 72 bytes of memory per 16 byte entry once loaded (measured: 9.01 MB for 131,000 entries), covering the map node, the
ArrayWrapperand its share of the table. At the 128K soft cap that is roughly 9 MB as a floor, not a ceiling:getInsertedCount()spans both buffers, so a window deferred past the cap can take this to about twice that - Space-proportional traffic handling
Uses read/write locks with SimpleTimer2 for thread safety and efficient buffer switching without synchronization overhead.
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionprivate static classThis saves the data as-is if the length is <= 8 bytes, otherwise it stores an 8-byte hash.private static final classWhat a decay found, carried out of the write lock so the caller can log it without extending the exclusive section.Field Summary
Modifier and TypeFieldDescriptionThe entries of the current period.private booleanTrue oncesetMaxEntries(int)has been used, meaning a caller has declared a memory ceiling to be the harder constraint.private longNanotime of the last decay, used to space decays at least one interval apart.private longNanotime of the last over-cap warning, so that a sustained flood does not log once per entry.private intThe cap actually in force:DEFAULT_MAX_ENTRIESunless a test asked for a smaller one so it can cross the threshold cheaply, or the router lowered it to hold memory down.The entries of the previous period.static final longMeasured retained cost per entry for 16 byte keys: about 72 bytes, covering the map node, theArrayWrapperand its share of the table.(package private) static final intMaximum entries before a forced early decay is attempted.Fields inherited from class DecayingBloomFilter
_context, _currentDuplicates, _decayEvent, _durationMs, _entryBytes, _keepDecaying, _log, _name, _reorganizeLock, MAX_MModifier and TypeFieldDescriptionprotected final I2PAppContextThe I2P app context.protected final AtomicLongCounter for current duplicates.protected final SimpleTimer2.TimedEventThe decay timer event.protected final intDuration of each filter in ms.protected final intSize of entries in bytes.protected booleanWhether decay is still running.protected final LogThe logger.protected final StringName for logging.protected final ReentrantReadWriteLockSynchronize against this lock when switching double buffers.static final intLargest exponent usable for a 32 byte key.Constructor Summary
ConstructorsConstructorDescriptionDecayingHashSet(I2PAppContext context, int durationMs, int entryBytes) Create a double-buffered hash set that will decay its entries over time.DecayingHashSet(I2PAppContext context, int durationMs, int entryBytes, String name) Decaying hash set with a custom name.DecayingHashSet(I2PAppContext context, int durationMs, int entryBytes, String name, int maxEntries) As above, with an explicit cap.Method Summary
Modifier and TypeMethodDescriptionbooleanadd(byte[] entry, int off, int len) Add the entry, returning whether it was a duplicate.booleanadd(long entry) Add the long entry, returning whether it was a duplicate.private booleanadd(long entry, boolean addIfNew) Adds one entry to the set.voidclear()Clear all filters and reset counts.protected voiddecay()Decay the filter, moving the current buffer to the previous.private voidForce a decay cycle if the entry count exceeds the soft cap.longBytes of memory the current contents occupy, at the measured rate of about 72 bytes per entry for 16 byte keys.doubleStubbed rate; only used for logging elsewhere.intUnsynchronized.intThe entry cap currently in force, across both buffers.booleanisKnown(byte[] entry, int off, int len) Check whether a byte array entry is already known, without adding it.booleanisKnown(long entry) Check whether the entry is already known, without adding it.private booleanlocked_add(DecayingHashSet.ArrayWrapper w, boolean addIfNew) Add the entry to the current set if new, or check membership only.private voidlogDecay(DecayingHashSet.DecayCounters counters) Caller must not hold the write lock.private voidRate limited to once per decay interval, so a flood cannot spam the log.voidsetMaxEntries(int maxEntries) Change the entry cap while running, for the router's low-memory path.voidSuper doesn't call clear, but neither do the users, so it seems like we should here.private DecayingHashSet.DecayCountersRetire the older buffer and clear the recycled one.Methods inherited from class DecayingBloomFilter
add, checkEntryBytes, getCurrentDuplicateCount, getLastWindowCount, getM, getMemoryBytes, getReadLock, getWriteLock, mForBudget, mForEntries, releaseReadLock, releaseWriteLockModifier and TypeMethodDescriptionbooleanadd(byte[] entry) Add a byte array entry to the filter.protected static voidcheckEntryBytes(int entryBytes) Validate an entry size for a bloom filter.longCurrent number of duplicates detected.intEntries seen in the most recently completed decay window, which is the count this filter has to be sized against.intgetM()Filter size exponent.longBytes of memory this filter occupies, for both buffers.protected voidAcquire the read lock.protected booleanAcquire the write lock.static intmForBudget(long budgetBytes) Largest exponent whose pair of buffers fits in the given byte budget, capped at the largest a 32 byte key can be hashed into.static intmForEntries(int entries, double targetFpr) Smallest exponent whose false positive rate stays at or under the target for the given number of entries, at the optimal k for that exponent.protected voidRelease the read lock.protected voidRelease the write lock.
Field Details
_current
The entries of the current period._previous
The entries of the previous period.DEFAULT_MAX_ENTRIES
static final int DEFAULT_MAX_ENTRIESMaximum entries before a forced early decay is attempted. This is a soft target, not a hard ceiling: memory is bounded by roughly two windows of traffic rather than by this number, because forcing a decay sooner than an entry's lifetime would drop live entries.Measured cost at this cap with 16 byte entries: about 9 MB, roughly 72 bytes per entry including the map node and the wrapper.
- See Also:
BYTES_PER_ENTRY
public static final long BYTES_PER_ENTRYMeasured retained cost per entry for 16 byte keys: about 72 bytes, covering the map node, theArrayWrapperand its share of the table. Measured 9.01 MB for 131,000 entries.- See Also:
_maxEntries
private volatile int _maxEntriesThe cap actually in force:DEFAULT_MAX_ENTRIESunless a test asked for a smaller one so it can cross the threshold cheaply, or the router lowered it to hold memory down.A set has no fixed memory cost, so unlike a bloom filter it can only be bounded by refusing growth: reaching the cap retires the older buffer even if its entries have not expired. That trades duplicate detection for a memory ceiling, so it is only the right call where the memory ceiling is the harder constraint. Volatile so the setter can change it while the router is running.
_hardCap
private volatile boolean _hardCapTrue oncesetMaxEntries(int)has been used, meaning a caller has declared a memory ceiling to be the harder constraint. The default cap stays soft: it is only a backstop, and preferring duplicate detection over an early retirement is the right default._lastDecayNanos
private volatile long _lastDecayNanosNanotime of the last decay, used to space decays at least one interval apart. Volatile so the over-cap path can test it without taking the write lock. Monotonic on purpose: a wall clock jump must not be able to force an early decay, which would drop entries that are still live._lastOverCapWarnNanos
private volatile long _lastOverCapWarnNanosNanotime of the last over-cap warning, so that a sustained flood does not log once per entry. Same clock as_lastDecayNanoson purpose, so one policy decision never mixes a monotonic and a wall clock reading. Volatile because the warn is reached from the unlocked pre-check inforceDecayIfOverCap(); a lost update here only costs an extra log line.
Constructor Details
DecayingHashSet
Create a double-buffered hash set that will decay its entries over time.- Parameters:
context- the app context supplying the log and the clockdurationMs- entries last for at least this long, but no more than twice this longentryBytes- how large are the entries to be added? 1 to 32 bytes- Throws:
IllegalArgumentException- if entryBytes is not 1-32
DecayingHashSet
Decaying hash set with a custom name.- Parameters:
context- the app context supplying the log and the clockdurationMs- entries last for at least this long, but no more than twice this longentryBytes- how large are the entries to be added? 1 to 32 bytesname- the instance name, used only in log messages and stats- Throws:
IllegalArgumentException- if entryBytes is not 1-32
DecayingHashSet
DecayingHashSet(I2PAppContext context, int durationMs, int entryBytes, String name, int maxEntries) As above, with an explicit cap. Only for tests, which need to cross the over-cap path without inserting the fullDEFAULT_MAX_ENTRIESentries.- Parameters:
context- the app context supplying the log and the clockdurationMs- entries last for at least this long, but no more than twice this longentryBytes- how large are the entries to be added? 1 to 32 bytesname- the instance name, used only in log messages and statsmaxEntries- hard cap on entries across both buffers
Method Details
getInsertedCount
public int getInsertedCount()Unsynchronized. Read on the add() path by forceDecayIfOverCap(), as well as for logging, so it is a hot call.- Overrides:
getInsertedCountin classDecayingBloomFilter- Returns:
- how many entries are held across both generations of the filter
getMaxEntries
public int getMaxEntries()The entry cap currently in force, across both buffers.- Returns:
- the maximum number of entries held across both buffers before a decay is triggered
setMaxEntries
public void setMaxEntries(int maxEntries) Change the entry cap while running, for the router's low-memory path.Lowering it takes effect at once: the next add() over the new cap triggers a decay, which may retire entries that have not yet expired. Raising it does not resurrect anything already retired.
- Parameters:
maxEntries- the new cap, must be positive- Throws:
IllegalArgumentException- if maxEntries is not positive
getEstimatedMemoryBytes
public long getEstimatedMemoryBytes()Bytes of memory the current contents occupy, at the measured rate of about 72 bytes per entry for 16 byte keys. Reported so a caller sizing against a memory budget can see the cost it is actually paying.- Returns:
- estimated retained bytes
getFalsePositiveRate
public double getFalsePositiveRate()Stubbed rate; only used for logging elsewhere.- Overrides:
getFalsePositiveRatein classDecayingBloomFilter- Returns:
- a probability from 0.0 to 1.0, computed for the current generation
add
public boolean add(byte[] entry, int off, int len) Add the entry, returning whether it was a duplicate.- Overrides:
addin classDecayingBloomFilter- Parameters:
entry- the entry dataoff- index in entry of the first byte to filterlen- number of bytes to filter, must equal _entryBytes- Returns:
- true if the entry added is a duplicate
add
public boolean add(long entry) Add the long entry, returning whether it was a duplicate.- Overrides:
addin classDecayingBloomFilter- Parameters:
entry- the long value to add- Returns:
- true if the entry added is a duplicate. the number of low order bits used is determined by the entryBytes parameter used on creation of the filter.
isKnown
public boolean isKnown(long entry) Check whether the entry is already known, without adding it.- Overrides:
isKnownin classDecayingBloomFilter- Parameters:
entry- the long value to check- Returns:
- true if the entry is already known. this does NOT add the entry however.
isKnown
public boolean isKnown(byte[] entry, int off, int len) Check whether a byte array entry is already known, without adding it.- Overrides:
isKnownin classDecayingBloomFilter- Parameters:
entry- the entry dataoff- index in entry of the first byte to filterlen- number of bytes to filter, must equal _entryBytes- Returns:
- true if the entry is already known. This does NOT add the entry however.
add
private boolean add(long entry, boolean addIfNew) Adds one entry to the set.- Parameters:
entry- the entry to addaddIfNew- true to add only when absent- Returns:
- true if the entry was added
forceDecayIfOverCap
private void forceDecayIfOverCap()Force a decay cycle if the entry count exceeds the soft cap.A decay must never run more often than the decay interval: the swap in
decay()drops the older buffer, and doing that before that buffer has aged out would forget entries that are still live, turning this duplicate/replay filter into a source of false negatives. So when the cap is exceeded we decay only if at least one interval has passed since the last decay, and otherwise leave it to the scheduled event.Best-effort: races are harmless (an extra decay, or a one-entry overshoot). Under sustained overload this holds up to about two windows of entries rather than the cap.
maybeWarnOverCap
private void maybeWarnOverCap()Rate limited to once per decay interval, so a flood cannot spam the log.locked_add
Add the entry to the current set if new, or check membership only.- Parameters:
w- the wrapped entry to add or test for membershipaddIfNew- if true, add the element to current if it is not already there or in previous; if false, only check- Returns:
- if the element is in either the current or previous set
clear
public void clear()Description copied from class:DecayingBloomFilterClear all filters and reset counts.- Overrides:
clearin classDecayingBloomFilter
stopDecaying
public void stopDecaying()Super doesn't call clear, but neither do the users, so it seems like we should here.- Overrides:
stopDecayingin classDecayingBloomFilter
decay
protected void decay()Description copied from class:DecayingBloomFilterDecay the filter, moving the current buffer to the previous.The buffer that becomes the new current one is reset to a freshly allocated array rather than zeroed in place. Zeroing in place is a memset of the whole filter held under the write lock, and every add() takes the read lock, so it stalls all inserts for its duration: measured at about 2.6ms at m=29. Allocating the replacement does the same zeroing without the lock and is in fact slightly cheaper at that size, so the lock is held only for the pointer swap. See
BloomSHA1.prepareReset()for the allocation/GC tradeoff this accepts.- Overrides:
decayin classDecayingBloomFilter
swapBuffers
Retire the older buffer and clear the recycled one. Caller must hold the write lock. Does no logging, so the lock is held only for the swap.- Returns:
- what was in the retiring buffer, for logging once unlocked
logDecay
Caller must not hold the write lock.- Parameters:
counters- the element and duplicate counts carried out of the decay, to log