All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.camunda.zeebe.broker.engine.impl.BoundedCommandCache Maven / Gradle / Ivy

There is a newer version: 8.7.0-alpha1
Show newest version
/*
 * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
 * one or more contributor license agreements. See the NOTICE file distributed
 * with this work for additional information regarding copyright ownership.
 * Licensed under the Camunda License 1.0. You may not use this file
 * except in compliance with the Camunda License 1.0.
 */
package io.camunda.zeebe.broker.engine.impl;

import io.camunda.zeebe.util.LockUtil;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.IntConsumer;
import org.agrona.collections.LongHashSet;

/**
 * A thread-safe, bounded command cache with light memory footprint, by storing keys in an
 * underlying {@link LongHashSet}. You can roughly estimate the memory usage of the set as the
 * capacity times 8 bytes (i.e. size of long).
 *
 * 

Thread-safety is guaranteed via naive locking, to be optimized if need be. * *

The set is bounded by performing random eviction to avoid going over capacity. Whenever new * keys are added, we calculate how many should be evicted beforehand, then randomly remove this * amount (since a set has no deterministic ordering). */ public final class BoundedCommandCache { private static final int DEFAULT_CAPACITY = 100_000; private final Lock lock = new ReentrantLock(); private final int capacity; private final LongHashSet cache; private final IntConsumer sizeReporter; public BoundedCommandCache(final int capacity) { this(capacity, ignored -> {}); } /** Returns a bounded cache which will report size changes to the given consumer. */ public BoundedCommandCache(final IntConsumer sizeReporter) { this(DEFAULT_CAPACITY, sizeReporter); } /** * You can estimate the size based on the capacity as followed. Since we use a {@link LongHashSet} * primitives, each element takes about 8 bytes. There is some minimal overhead for state * management and the likes, which means in the end, amortized, each entry takes about 8.4 bytes. * *

So the default capacity, 100,000 entries, will use about 840KB of memory, even when full. * * @param capacity the maximum capacity of the command cache */ public BoundedCommandCache(final int capacity, final IntConsumer sizeReporter) { this.capacity = capacity; this.sizeReporter = sizeReporter; // to avoid resizing, we set a load factor of 0.9, and increase the internal capacity // preemptively final var resizeThreshold = (int) Math.ceil(capacity * 0.9f); final var capacityToPreventResize = 2 * capacity - resizeThreshold; cache = new LongHashSet(capacityToPreventResize, 0.9f, true); sizeReporter.accept(0); } public void add(final LongHashSet keys) { LockUtil.withLock(lock, () -> lockedAdd(keys)); } public boolean contains(final long key) { return LockUtil.withLock(lock, () -> cache.contains(key)); } public void remove(final long key) { LockUtil.withLock( lock, () -> { cache.remove(key); sizeReporter.accept(cache.size()); }); } public int size() { return LockUtil.withLock(lock, cache::size); } public void clear() { LockUtil.withLock( lock, () -> { cache.clear(); sizeReporter.accept(0); }); } private void lockedAdd(final LongHashSet keys) { final int evictionCount = cache.size() + keys.size() - capacity; if (evictionCount > 0) { evict(evictionCount); } cache.addAll(keys); sizeReporter.accept(cache.size()); } private void evict(final int count) { final var evictionStartIndex = ThreadLocalRandom.current().nextInt(0, capacity - count + 1); final int evictionEndIndex = evictionStartIndex + count; final var iterator = cache.iterator(); for (int i = 0; i < evictionEndIndex && iterator.hasNext(); i++) { iterator.next(); if (i >= evictionStartIndex) { iterator.remove(); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy