nstream.persist.kv.state.MapChangesets Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of nstream-persist-kv Show documentation
Show all versions of nstream-persist-kv Show documentation
Adapter for persistence implementations based on key-value stores
// Copyright 2015-2024 Nstream, inc.
//
// 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 nstream.persist.kv.state;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.function.UnaryOperator;
import nstream.persist.api.PersistenceException;
import swim.collections.HashTrieMap;
import swim.structure.Value;
/**
* Keeps track of changes to all map states in the {@link StoreState}.
*/
public class MapChangesets {
@SuppressWarnings("unchecked")
private static final AtomicReferenceFieldUpdater>> CHANGESETS =
AtomicReferenceFieldUpdater.newUpdater(MapChangesets.class, (Class>>) (Class>) HashTrieMap.class, "changesets");
private volatile HashTrieMap> changesets;
public MapChangesets() {
this.changesets = HashTrieMap.empty();
}
private static long withChangeset(AtomicReference changeset, UnaryOperator op) {
MapChangeset oldChangeset, newChangeset;
do {
oldChangeset = changeset.get();
//If the change-set is null, it has been consumed so we return Long.MIN_VALUE to indicate they outer loop should retry.
if (oldChangeset == null) {
return Long.MIN_VALUE;
}
newChangeset = op.apply(oldChangeset);
} while (!changeset.compareAndSet(oldChangeset, newChangeset));
return newChangeset.getCommitSize() - oldChangeset.getCommitSize();
}
private static long updateChangeset(AtomicReference changeset, long epoch, Value key, long valueSize) {
return withChangeset(changeset, (oldChangeSet) -> oldChangeSet.added(epoch, key, valueSize));
}
private static long clearChangeset(AtomicReference changeset, long epoch) {
return withChangeset(changeset, (oldChangeSet) -> oldChangeSet.cleared(epoch));
}
private static HashMap toMap(HashTrieMap> oldChangesets) throws PersistenceException {
if (oldChangesets == null) {
throw new PersistenceException("Already closed.");
}
final var consumed = new HashMap(oldChangesets.size());
for (Map.Entry> entry : oldChangesets.entrySet()) {
consumed.put(entry.getKey(), entry.getValue().getAndSet(null));
}
return consumed;
}
/**
* Consume all change-sets in the map.
*
* @return Map associating database IDs with change-sets.
*/
public HashMap consume() throws PersistenceException {
final var oldChangesets = CHANGESETS.getAndSet(this, HashTrieMap.empty());
return toMap(oldChangesets);
}
/**
* Consume all changesets and do not accept any more.
*
* @return Map associating database IDs with change-sets.
*/
public HashMap close() throws PersistenceException {
final var oldChangesets = CHANGESETS.getAndSet(this, null);
return toMap(oldChangesets);
}
/**
* Add an update to a changeset in the map.
*
* @param id The database ID of the lane.
* @param epoch The monotonic epoch (for the lane).
* @param key The key of the entry that was updated.
* @param valueSize The size value that was updated in bytes ({@code 0} for a removal).
* @return The change in the total size of all changes in the map.
*/
public long update(long id, long epoch, Value key, long valueSize) throws PersistenceException {
long delta;
boolean done;
do {
final var oldChangesets = CHANGESETS.get(this);
if (oldChangesets == null) {
throw new PersistenceException("Already closed.");
}
final var changeset = oldChangesets.get(id);
if (changeset == null) {
final var newChangeset = new MapChangeset(epoch).added(epoch, key, valueSize);
final var newChangesets = oldChangesets.updated(id, new AtomicReference<>(newChangeset));
done = CHANGESETS.compareAndSet(this, oldChangesets, newChangesets);
delta = newChangeset.getCommitSize();
} else {
//This will return Long.MIN_VALUE if the changeset has been removed since we read it which means we need to retry.
delta = updateChangeset(changeset, epoch, key, valueSize);
done = delta != Long.MIN_VALUE;
}
} while (!done);
return delta;
}
/**
* Clear the change-set for a lane.
*
* @param id The database ID of the lane.
* @param epoch The monotonic epoch (for the lane).
* @return The change in the total size of all changes in the map.
*/
public long clear(long id, long epoch) throws PersistenceException {
long delta;
boolean done;
do {
final var oldChangesets = CHANGESETS.get(this);
if (oldChangesets == null) {
throw new PersistenceException("Already closed.");
}
final var changeset = oldChangesets.get(id);
if (changeset == null) {
final var newChangeset = new MapChangeset(epoch).cleared(epoch);
final var newChangesets = oldChangesets.updated(id, new AtomicReference<>(newChangeset));
done = CHANGESETS.compareAndSet(this, oldChangesets, newChangesets);
delta = newChangeset.getCommitSize();
} else {
//This will return Long.MIN_VALUE if the changeset has been removed since we read it which means we need to retry.
delta = clearChangeset(changeset, epoch);
done = delta != Long.MIN_VALUE;
}
} while (!done);
return delta;
}
}