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

io.questdb.std.ConcurrentAssociativeCache Maven / Gradle / Ivy

/*******************************************************************************
 *     ___                  _   ____  ____
 *    / _ \ _   _  ___  ___| |_|  _ \| __ )
 *   | | | | | | |/ _ \/ __| __| | | |  _ \
 *   | |_| | |_| |  __/\__ \ |_| |_| | |_) |
 *    \__\_\\__,_|\___||___/\__|____/|____/
 *
 *  Copyright (c) 2014-2019 Appsicle
 *  Copyright (c) 2019-2024 QuestDB
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 ******************************************************************************/

package io.questdb.std;

import io.questdb.metrics.Counter;
import io.questdb.metrics.LongGauge;
import io.questdb.metrics.NullCounter;
import io.questdb.metrics.NullLongGauge;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * Thread-safe cache implementation.
 * 

* The cache is organized in rows (think, hash table buckets), each holding a fixed number * of blocks (items). A row is represented with an array of string keys and an array of * cached object values. The arrays are filled left-to-right. *

* When an object is taken from the cache, its key is kept around. This way, when the object * is later returned, there is a chance that we're able to reuse the key and avoid String * allocation. Objects are always inserted to the first block which means that eviction * in each row is FIFO. *

* The cache uses striped locking, i.e. each row is synchronized with its own lock. */ public class ConcurrentAssociativeCache implements AssociativeCache { private static final int MIN_BLOCKS = 1; private static final int MIN_ROWS = 1; private final int blocks; private final LongGauge cachedGauge; private final Counter hitCounter; // Each row has its own array of keys. // Arrays are also used for locking. private final ObjList keys; private final Counter missCounter; private final int rowMask; private final int rows; // Each row has its own array of values. private final ObjList values; public ConcurrentAssociativeCache(int blocks, int rows) { this(blocks, rows, NullLongGauge.INSTANCE, NullCounter.INSTANCE, NullCounter.INSTANCE); } @SuppressWarnings("unchecked") public ConcurrentAssociativeCache(int blocks, int rows, LongGauge cachedGauge, Counter hitCounter, Counter missCounter) { this.blocks = Math.max(MIN_BLOCKS, Numbers.ceilPow2(blocks)); this.rows = Math.max(MIN_ROWS, Numbers.ceilPow2(rows)); int capacity = this.rows * this.blocks; if (capacity < 0) { throw new OutOfMemoryError(); } this.keys = new ObjList<>(this.rows); this.values = new ObjList<>(this.rows); for (int i = 0; i < this.rows; i++) { keys.add(new String[this.blocks]); values.add((V[]) new Object[this.blocks]); } this.rowMask = this.rows - 1; this.cachedGauge = cachedGauge; this.hitCounter = hitCounter; this.missCounter = missCounter; } @Override public int capacity() { return rows * blocks; } @Override public void clear() { long freed = 0; for (int i = 0; i < rows; i++) { final String[] rowKeys = keys.getQuick(i); final V[] rowValues = values.getQuick(i); synchronized (rowKeys) { for (int j = 0; j < blocks; j++) { if (rowKeys[j] != null) { rowKeys[j] = null; if (rowValues[j] != null) { rowValues[j] = Misc.freeIfCloseable(rowValues[j]); freed++; } } } } } cachedGauge.add(-freed); } @Override public void close() { clear(); } @Override public V poll(@NotNull CharSequence key) { final int row = row(key); final String[] rowKeys = keys.getQuick(row); final V[] rowValues = values.getQuick(row); V value = null; synchronized (rowKeys) { for (int i = 0; i < blocks; i++) { if (rowKeys[i] == null) { break; } if (rowValues[i] != null && Chars.equals(key, rowKeys[i])) { value = rowValues[i]; rowValues[i] = null; break; } } } if (value != null) { // The value is present, so we're decrementing the gauge. cachedGauge.dec(); hitCounter.inc(); } else { missCounter.inc(); } // We do not null the key reference to avoid creating another immutable key. return value; } @Override public void put(@NotNull CharSequence key, @Nullable V value) { final int row = row(key); final String[] rowKeys = keys.getQuick(row); final V[] rowValues = values.getQuick(row); V outgoingValue; synchronized (rowKeys) { // Find a block to place the object. int idx = blocks - 1; for (int i = 0; i < blocks; i++) { if (rowKeys[i] == null) { // Empty block found. idx = i; break; } if (rowValues[i] == null) { // The value was previously cleared by poll(). idx = i; if (Chars.equals(key, rowKeys[i])) { // That's our key, so reuse it to avoid String allocation. key = rowKeys[i]; break; } } } // Evict object at the found block (or the very last block). outgoingValue = rowValues[idx]; // Shift the arrays to be able to insert to the first block. System.arraycopy(rowKeys, 0, rowKeys, 1, idx); System.arraycopy(rowValues, 0, rowValues, 1, idx); // We can now insert the new object. rowKeys[0] = Chars.toString(key); rowValues[0] = value; } if (outgoingValue == null) { // We're inserting. cachedGauge.inc(); } else { // We're replacing the value with another one, no need to change the gauge. Misc.freeIfCloseable(outgoingValue); } } private int row(CharSequence key) { return Hash.spread(Chars.hashCode(key)) & rowMask; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy