/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
* Copyright 2015 ForgeRock AS
*/
package org.opends.server.backends.pluggable;
import static org.opends.server.backends.pluggable.CursorTransformer.*;
import java.util.NoSuchElementException;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ByteStringBuilder;
import org.forgerock.util.Function;
import org.forgerock.util.promise.NeverThrowsException;
import org.opends.server.backends.pluggable.CursorTransformer.ValueTransformer;
import org.opends.server.backends.pluggable.OnDiskMergeImporter.SequentialCursorDecorator;
import org.opends.server.backends.pluggable.spi.Cursor;
import org.opends.server.backends.pluggable.spi.Importer;
import org.opends.server.backends.pluggable.spi.ReadableTransaction;
import org.opends.server.backends.pluggable.spi.SequentialCursor;
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.backends.pluggable.spi.UpdateFunction;
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
/**
* Store counters associated to a key. Counter value is sharded amongst multiple keys to allow concurrent update without
* contention (at the price of a slower read).
*/
final class ShardedCounter extends AbstractTree
{
/**
* Must be a power of 2
* @see <a href="http://en.wikipedia.org/wiki/Modulo_operation#Performance_issues">Performance issues</a>
*/
private static final long SHARD_COUNT = 256;
private static final ValueTransformer<ByteString, ByteString, Long, NeverThrowsException> TO_LONG =
new ValueTransformer<ByteString, ByteString, Long, NeverThrowsException>()
{
@Override
public Long transform(ByteString key, ByteString value)
{
return decodeValue(value);
}
};
private static final Function<ByteString, ByteString, NeverThrowsException> TO_KEY =
new Function<ByteString, ByteString, NeverThrowsException>()
{
@Override
public ByteString apply(ByteString shardedKey)
{
// -1 to remove the shard id.
return shardedKey.subSequence(0, shardedKey.length() - 1);
}
};
ShardedCounter(TreeName name)
{
super(name);
}
SequentialCursor<ByteString, Void> openCursor(ReadableTransaction txn)
{
return new UniqueKeysCursor<>(transformKeysAndValues(
txn.openCursor(getName()), TO_KEY,
CursorTransformer.<ByteString, ByteString, Void> constant(null)));
}
private Cursor<ByteString, Long> openCursor0(ReadableTransaction txn)
{
return transformKeysAndValues(txn.openCursor(getName()), TO_KEY, TO_LONG);
}
void addCount(final WriteableTransaction txn, ByteSequence key, final long delta)
{
txn.update(getName(), getShardedKey(key), new UpdateFunction()
{
@Override
public ByteSequence computeNewValue(ByteSequence oldValue)
{
final long currentValue = oldValue == null ? 0 : decodeValue(oldValue.toByteString());
return encodeValue(currentValue + delta);
}
});
}
void importPut(Importer importer, ByteSequence key, long delta)
{
if (delta != 0)
{
importer.put(getName(), getShardedKey(key), encodeValue(delta));
}
}
long getCount(final ReadableTransaction txn, ByteSequence key)
{
long counterValue = 0;
try (final SequentialCursor<ByteString, Long> cursor = new ShardCursor(openCursor0(txn), key))
{
while (cursor.next())
{
counterValue += cursor.getValue();
}
}
return counterValue;
}
long removeCount(final WriteableTransaction txn, ByteSequence key)
{
long counterValue = 0;
try (final SequentialCursor<ByteString, Long> cursor = new ShardCursor(openCursor0(txn), key))
{
// Iterate over and remove all the thread local shards
while (cursor.next())
{
counterValue += cursor.getValue();
cursor.delete();
}
}
return counterValue;
}
static long decodeValue(ByteString value)
{
switch (value.length())
{
case 1:
return value.byteAt(0);
case (Integer.SIZE / Byte.SIZE):
return value.toInt();
case (Long.SIZE / Byte.SIZE):
return value.toLong();
default:
throw new IllegalArgumentException("Unsupported sharded-counter value format.");
}
}
static ByteString encodeValue(long value)
{
final byte valueAsByte = (byte) value;
if (valueAsByte == value)
{
return ByteString.wrap(new byte[] { valueAsByte });
}
final int valueAsInt = (int) value;
if (valueAsInt == value)
{
return ByteString.valueOfInt(valueAsInt);
}
return ByteString.valueOfLong(value);
}
@Override
public String valueToString(ByteString value)
{
return String.valueOf(decodeValue(value));
}
private static ByteSequence getShardedKey(ByteSequence key)
{
final byte bucket = (byte) (Thread.currentThread().getId() & (SHARD_COUNT - 1));
return new ByteStringBuilder(key.length() + ByteStringBuilder.MAX_COMPACT_SIZE).appendBytes(key).appendByte(bucket);
}
/** Restricts a cursor to the shards of a specific key. */
private final class ShardCursor extends SequentialCursorDecorator<Cursor<ByteString, Long>, ByteString, Long>
{
private final ByteSequence targetKey;
private boolean initialized;
ShardCursor(Cursor<ByteString, Long> delegate, ByteSequence targetKey)
{
super(delegate);
this.targetKey = targetKey;
}
@Override
public boolean next()
{
if (!initialized)
{
initialized = true;
return delegate.positionToKeyOrNext(targetKey) && isOnTargetKey();
}
return delegate.next() && isOnTargetKey();
}
private boolean isOnTargetKey()
{
return targetKey.equals(delegate.getKey());
}
}
/**
* Cursor that returns unique keys and null values. Ensure that {@link #getKey()} will return a different key after
* each {@link #next()}.
*/
static final class UniqueKeysCursor<K> implements SequentialCursor<K, Void>
{
private final Cursor<K, ?> delegate;
private boolean isDefined;
private K key;
UniqueKeysCursor(Cursor<K, ?> cursor)
{
this.delegate = cursor;
if (!delegate.isDefined())
{
delegate.next();
}
}
@Override
public boolean next()
{
isDefined = delegate.isDefined();
if (isDefined)
{
key = delegate.getKey();
skipEntriesWithSameKey();
}
return isDefined;
}
private void skipEntriesWithSameKey()
{
throwIfUndefined(this);
while (delegate.next() && key.equals(delegate.getKey()))
{
// Skip all entries having the same key.
}
// Delegate is one step beyond. When delegate.isDefined() return false, we have to return true once more.
isDefined = true;
}
@Override
public boolean isDefined()
{
return isDefined;
}
@Override
public K getKey() throws NoSuchElementException
{
throwIfUndefined(this);
return key;
}
@Override
public Void getValue() throws NoSuchElementException
{
throwIfUndefined(this);
return null;
}
@Override
public void delete() throws NoSuchElementException, UnsupportedOperationException
{
throw new UnsupportedOperationException();
}
@Override
public void close()
{
key = null;
delegate.close();
}
private static void throwIfUndefined(SequentialCursor<?, ?> cursor)
{
if (!cursor.isDefined())
{
throw new NoSuchElementException();
}
}
}
}