CEP-15 (Accord): When starting a transaction in a table where Accord is not enabled, should fail fast rather than fail with lack of ranges

patch by Youki Shiraishi; reviewed by Caleb Rackliffe, David Capwell for CASSANDRA-19759
This commit is contained in:
Youki Shiraishi 2024-07-23 12:50:58 -07:00 committed by David Capwell
parent 1439fe8d31
commit 6d01bc2535
48 changed files with 1938 additions and 2334 deletions

View File

@ -1,19 +1,14 @@
ccm create accord-cql-poc -n 3
ccm start
bin/cqlsh -e "create keyspace ks with replication={'class':'SimpleStrategy', 'replication_factor':3};"
bin/cqlsh -e "create table ks.tbl1 (k int primary key, v int);"
bin/cqlsh -e "create table ks.tbl2 (k int primary key, v int);"
bin/nodetool -h 0000:0000:0000:0000:0000:ffff:7f00:0001 -p 7100 createepochunsafe
bin/nodetool -h 0000:0000:0000:0000:0000:ffff:7f00:0001 -p 7200 createepochunsafe
bin/nodetool -h 0000:0000:0000:0000:0000:ffff:7f00:0001 -p 7300 createepochunsafe
bin/cqlsh -e "CREATE KEYSPACE ks WITH replication={'class':'SimpleStrategy', 'replication_factor':3};"
bin/cqlsh -e "CREATE TABLE ks.tbl1 (k int PRIMARY KEY, v int) WITH transactional_mode = 'full';"
bin/cqlsh -e "CREATE TABLE ks.tbl2 (k int PRIMARY KEY, v int) WITH transactional_mode = 'full';"
BEGIN TRANSACTION
LET row1 = (SELECT * FROM ks.tbl1 WHERE k = 1);
SELECT row1.v;
IF row1 IS NULL THEN
INSERT INTO ks.tbl1 (k, v) VALUES (1, 2);
INSERT INTO ks.tbl2 (k, v) VALUES (1, 2);
END IF
COMMIT TRANSACTION;
COMMIT TRANSACTION;

@ -1 +1 @@
Subproject commit 694ae39e2e00075bdabd47632dced0db12a9981d
Subproject commit 4c870dc9b561a841ea9b923ff739953adcc00325

View File

@ -214,6 +214,7 @@ public enum CassandraRelevantProperties
DRAIN_EXECUTOR_TIMEOUT_MS("cassandra.drain_executor_timeout_ms", convertToString(TimeUnit.MINUTES.toMillis(5))),
DROP_OVERSIZED_READ_REPAIR_MUTATIONS("cassandra.drop_oversized_readrepair_mutations"),
DTEST_ACCORD_ENABLED("jvm_dtest.accord.enabled", "true"),
DTEST_ACCORD_JOURNAL_SANITY_CHECK_ENABLED("jvm_dtest.accord.journal_sanity_check_enabled", "false"),
DTEST_API_LOG_TOPOLOGY("cassandra.dtest.api.log.topology"),
/** This property indicates if the code is running under the in-jvm dtest framework */
DTEST_IS_IN_JVM_DTEST("org.apache.cassandra.dtest.is_in_jvm_dtest"),

View File

@ -101,6 +101,7 @@ public class TransactionStatement implements CQLStatement.CompositeCQLStatement,
public static final String INCOMPLETE_PRIMARY_KEY_SELECT_MESSAGE = "SELECT must specify either all primary key elements or all partition key elements and LIMIT 1. In both cases partition key elements must be always specified with equality operators; %s %s";
public static final String NO_CONDITIONS_IN_UPDATES_MESSAGE = "Updates within transactions may not specify their own conditions; %s statement %s";
public static final String NO_TIMESTAMPS_IN_UPDATES_MESSAGE = "Updates within transactions may not specify custom timestamps; %s statement %s";
public static final String TRANSACTIONS_DISABLED_ON_TABLE_MESSAGE = "Accord transactions are disabled on table (See transactional_mode in table options); %s statement %s";
public static final String NO_COUNTERS_IN_TXNS_MESSAGE = "Counter columns cannot be accessed within a transaction; %s statement %s";
public static final String EMPTY_TRANSACTION_MESSAGE = "Transaction contains no reads or writes";
public static final String SELECT_REFS_NEED_COLUMN_MESSAGE = "SELECT references must specify a column.";
@ -529,6 +530,8 @@ public class TransactionStatement implements CQLStatement.CompositeCQLStatement,
SelectStatement prepared = select.prepare(bindVariables);
if (!prepared.table.isAccordEnabled())
throw invalidRequest(TRANSACTIONS_DISABLED_ON_TABLE_MESSAGE, "SELECT", prepared.source);
if (prepared.table.isCounter())
throw invalidRequest(NO_COUNTERS_IN_TXNS_MESSAGE, "SELECT", prepared.source);
@ -547,6 +550,8 @@ public class TransactionStatement implements CQLStatement.CompositeCQLStatement,
{
SelectStatement prepared = select.prepare(bindVariables);
if (!prepared.table.isAccordEnabled())
throw invalidRequest(TRANSACTIONS_DISABLED_ON_TABLE_MESSAGE, "SELECT", prepared.source);
if (prepared.table.isCounter())
throw invalidRequest(NO_COUNTERS_IN_TXNS_MESSAGE, "SELECT", prepared.source);
@ -572,6 +577,7 @@ public class TransactionStatement implements CQLStatement.CompositeCQLStatement,
ModificationStatement.Parsed parsed = updates.get(i);
ModificationStatement prepared = parsed.prepare(state, bindVariables);
checkTrue(prepared.metadata().isAccordEnabled(), TRANSACTIONS_DISABLED_ON_TABLE_MESSAGE, prepared.type, prepared.source);
checkFalse(prepared.hasConditions(), NO_CONDITIONS_IN_UPDATES_MESSAGE, prepared.type, prepared.source);
checkFalse(prepared.isTimestampSet(), NO_TIMESTAMPS_IN_UPDATES_MESSAGE, prepared.type, prepared.source);

View File

@ -260,6 +260,16 @@ final class ActiveSegment<K, V> extends Segment<K, V>
return lastFlushedOffset < allocatePosition;
}
public boolean isFlushed(long position)
{
return lastFlushedOffset >= position;
}
public long lastFlushedOffset()
{
return lastFlushedOffset;
}
/**
* Possibly force a disk flush for this segment file.
* TODO FIXME: calls from outside Flusher + callbacks
@ -329,14 +339,9 @@ final class ActiveSegment<K, V> extends Segment<K, V>
}
}
boolean isCompletedAndFullyFlushed(int syncedOffset)
boolean isFullyFlushed()
{
return syncedOffset >= endOfBuffer;
}
boolean isCompletedAndFullyFsynced()
{
return lastFsyncOffset >= endOfBuffer;
return lastFsyncOffset >= allocatePosition.get();
}
/**
@ -427,17 +432,35 @@ final class ActiveSegment<K, V> extends Segment<K, V>
private final OpOrder.Group appendOp;
private final ByteBuffer buffer;
private final int position;
private final int size;
Allocation(OpOrder.Group appendOp, ByteBuffer buffer)
{
this.appendOp = appendOp;
this.buffer = buffer;
this.position = buffer.position();
this.size = buffer.remaining();
}
void write(K id, ByteBuffer record, Set<Integer> hosts)
RecordPointer write(K id, ByteBuffer record, Set<Integer> hosts)
{
try (BufferedDataOutputStreamPlus out = new DataOutputBufferFixed(buffer))
{
EntrySerializer.write(id, record, hosts, keySupport, out, descriptor.userVersion);
index.update(id, position);
metadata.update(hosts);
return new RecordPointer(descriptor.timestamp, position);
}
catch (IOException e)
{
throw new JournalWriteError(descriptor, file, e);
}
finally
{
appendOp.close();
}
}
// Variant of write that does not allocate/return a record pointer
void writeInternal(K id, ByteBuffer record, Set<Integer> hosts)
{
try (BufferedDataOutputStreamPlus out = new DataOutputBufferFixed(buffer))
{
@ -455,21 +478,6 @@ final class ActiveSegment<K, V> extends Segment<K, V>
}
}
void asyncWrite(K id, V record, ByteBuffer bytes, Set<Integer> hosts, Object writeContext, AsyncCallbacks<K, V> callbacks) throws IOException
{
try (BufferedDataOutputStreamPlus out = new DataOutputBufferFixed(buffer))
{
EntrySerializer.write(id, bytes, hosts, keySupport, out, descriptor.userVersion);
index.update(id, position);
metadata.update(hosts);
callbacks.onWrite(descriptor.timestamp, position, size, id, record, writeContext);
}
finally
{
appendOp.close();
}
}
void awaitFlush(Timer waitingOnFlush)
{
try (Timer.Context ignored = waitingOnFlush.time())

View File

@ -1,45 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cassandra.journal;
public interface AsyncCallbacks<K, V>
{
/**
* Invoked once an entry has been written to the file, and indexes have been updated, but before it
* has been flushed to disk. Invoked from the writer thread. Execution order of onWrite() callbacks
* with regard to each other is undefined.
*/
void onWrite(long segment, int position, int size, K key, V value, Object writeContext);
/**
* Invoked when anything goes wrong with writing the entry - anywhere from serialization to writing to the file,
* to requesting the flush.
*/
void onWriteFailed(K key, V value, Object writeContext, Throwable cause);
/**
* Invoked after {@link Flusher} successfully flushes a segment or multiple segments to disk.
* Invocation of this callback implies that any segments older than {@code segment} have been
* completed and also flushed.
* Invocation of this callback also implies that all {@link #onWrite(long, int, int, Object, Object, Object)}
* callbacks for all entries earlier than (segment, position) have finished execution.
*/
void onFlush(long segment, int position);
void onFlushFailed(Throwable cause);
}

View File

@ -60,7 +60,6 @@ final class Flusher<K, V>
private final Journal<K, V> journal;
private final Params params;
private final AsyncCallbacks<K, V> callbacks;
private volatile Interruptible flushExecutor;
private volatile Interruptible fsyncExecutor;
@ -84,14 +83,15 @@ final class Flusher<K, V>
private final FlushMethod<K, V> syncFlushMethod;
private final FlushMethod<K, V> asyncFlushMethod;
private final Callbacks callbacks;
Flusher(Journal<K, V> journal)
Flusher(Journal<K, V> journal, Callbacks callbacks)
{
this.journal = journal;
this.params = journal.params;
this.callbacks = journal.callbacks;
this.syncFlushMethod = syncFlushMethod(params);
this.asyncFlushMethod = asyncFlushMethod(params);
this.callbacks = callbacks;
}
void start()
@ -304,6 +304,9 @@ final class Flusher<K, V>
{
if (synchronousFsync) fsyncFinishedFor = startedAt;
else fSyncRunnable.doNoOpFlush(startedAt);
if (current != null)
callbacks.onFlush(current.descriptor.timestamp, (int) current.lastFlushedOffset());
return;
}
@ -509,4 +512,17 @@ final class Flusher<K, V>
{
return written.get();
}
}
public interface Callbacks
{
/**
* Invoked after {@link Flusher} successfully flushes a segment or multiple segments to disk.
* Invocation of this callback implies that any segments older than {@code segment} have been
* completed and also flushed.
* callbacks for all entries earlier than (segment, position) have finished execution.
*/
void onFlush(long segment, int position);
void onFlushFailed(Throwable cause);
}
}

View File

@ -102,6 +102,12 @@ final class InMemoryIndex<K> extends Index<K>
return offests.length == 0 ? -1 : offests[0];
}
@Override
int[] lookUpAll(K id)
{
return lookUp(id);
}
public void persist(Descriptor descriptor)
{
File tmpFile = descriptor.tmpFileFor(Component.INDEX);

View File

@ -51,6 +51,7 @@ abstract class Index<K> implements Closeable
* @return the first offset into the segment, or -1 is none were found
*/
abstract int lookUpFirst(K id);
abstract int[] lookUpAll(K id);
/**
* @return the first (smallest) id in the index

View File

@ -23,15 +23,18 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.zip.CRC32;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -53,6 +56,7 @@ import org.apache.cassandra.utils.Crc;
import org.apache.cassandra.utils.JVMStabilityInspector;
import org.apache.cassandra.utils.Simulate;
import org.apache.cassandra.utils.concurrent.WaitQueue;
import org.jctools.queues.MpscUnboundedArrayQueue;
import static java.lang.String.format;
import static java.util.Comparator.comparing;
@ -88,7 +92,6 @@ public class Journal<K, V> implements Shutdownable
final String name;
final File directory;
final Params params;
final AsyncCallbacks<K, V> callbacks;
final KeySupport<K> keySupport;
final ValueSerializer<K, V> valueSerializer;
@ -112,31 +115,95 @@ public class Journal<K, V> implements Shutdownable
private final WaitQueue segmentPrepared = newWaitQueue();
private final WaitQueue allocatorThreadWaitQueue = newWaitQueue();
private final BooleanSupplier allocatorThreadWaitCondition = () -> (availableSegment == null);
private final FlusherCallbacks flusherCallbacks;
SequentialExecutorPlus closer;
//private final Set<Descriptor> invalidations = Collections.newSetFromMap(new ConcurrentHashMap<>());
private class FlusherCallbacks implements Flusher.Callbacks
{
private final MpscUnboundedArrayQueue<WaitingFor> waitingFor = new MpscUnboundedArrayQueue<>(256);
private List<WaitingFor> drained = new ArrayList<>();
@Override
public void onFlush(long segment, int position)
{
waitingFor.drain(drained::add);
List<WaitingFor> remaining = new ArrayList<>();
for (WaitingFor wait : drained)
{
if (wait.segment == segment && wait.position <= position)
wait.run();
else
remaining.add(wait);
}
drained = remaining;
}
@Override
public void onFlushFailed(Throwable cause)
{
// TODO: panic
}
private void submit(RecordPointer pointer, Runnable runnable)
{
if (isFlushed(pointer))
runnable.run();
else
{
waitingFor.add(new WaitingFor(pointer.segment, pointer.position, runnable));
flusher.requestExtraFlush();
}
}
}
private static class WaitingFor extends RecordPointer implements Runnable
{
private final Runnable onFlush;
public WaitingFor(long segment, int position, Runnable onFlush)
{
super(segment, position);
this.onFlush = onFlush;
}
public void run()
{
onFlush.run();
}
}
public Journal(String name,
File directory,
Params params,
AsyncCallbacks<K, V> callbacks,
KeySupport<K> keySupport,
ValueSerializer<K, V> valueSerializer)
{
this.name = name;
this.directory = directory;
this.params = params;
this.callbacks = callbacks;
this.keySupport = keySupport;
this.valueSerializer = valueSerializer;
this.metrics = new Metrics<>(name);
this.flusher = new Flusher<>(this);
this.flusherCallbacks = new FlusherCallbacks();
this.flusher = new Flusher<>(this, flusherCallbacks);
//this.invalidator = new Invalidator<>(this);
//this.compactor = new Compactor<>(this);
}
public boolean isFlushed(RecordPointer recordPointer)
{
return segments.get().isFlushed(recordPointer);
}
public void onFlush(RecordPointer recordPointer, Runnable runnable)
{
flusherCallbacks.submit(recordPointer, runnable);
}
public void start()
{
metrics.register(flusher);
@ -267,6 +334,34 @@ public class Journal<K, V> implements Shutdownable
return null;
}
// TODO: This should be improved with new index that should take better care of handling multiple items
public List<V> readAll(K id)
{
EntrySerializer.EntryHolder<K> holder = new EntrySerializer.EntryHolder<>();
List<V> res = new ArrayList<>(2);
try (ReferencedSegments<K, V> segments = selectAndReference(id))
{
for (Segment<K, V> segment : segments.all())
{
segment.readAll(id, holder, () -> {
try (DataInputBuffer in = new DataInputBuffer(holder.value, false))
{
Invariants.checkState(Objects.equals(holder.key, id),
"%s != %s", holder.key, id);
res.add(valueSerializer.deserialize(holder.key, in, segment.descriptor.userVersion));
holder.clear();
}
catch (IOException e)
{
// can only throw if serializer is buggy
throw new RuntimeException(e);
}
});
}
}
return res;
}
/**
* Looks up a record by the provided id, if the value satisfies the provided condition.
* <p/>
@ -371,13 +466,13 @@ public class Journal<K, V> implements Shutdownable
* @param record the record to store
* @param hosts hosts expected to invalidate the record
*/
public void write(K id, V record, Set<Integer> hosts)
public void blockingWrite(K id, V record, Set<Integer> hosts)
{
try (DataOutputBuffer dob = DataOutputBuffer.scratchBuffer.get())
{
valueSerializer.serialize(id, record, dob, params.userVersion());
ActiveSegment<K, V>.Allocation alloc = allocate(dob.getLength(), hosts);
alloc.write(id, dob.unsafeGetBufferAndFlip(), hosts);
alloc.writeInternal(id, dob.unsafeGetBufferAndFlip(), hosts);
flusher.waitForFlush(alloc);
}
catch (IOException e)
@ -397,19 +492,22 @@ public class Journal<K, V> implements Shutdownable
* @param record the record to store
* @param hosts hosts expected to invalidate the record
*/
public void asyncWrite(K id, V record, Set<Integer> hosts, Object writeContext)
public RecordPointer asyncWrite(K id, V record, Set<Integer> hosts)
{
RecordPointer recordPointer;
try (DataOutputBuffer dob = DataOutputBuffer.scratchBuffer.get())
{
valueSerializer.serialize(id, record, dob, params.userVersion());
ActiveSegment<K, V>.Allocation alloc = allocate(dob.getLength(), hosts);
alloc.asyncWrite(id, record, dob.unsafeGetBufferAndFlip(), hosts, writeContext, callbacks);
recordPointer = alloc.write(id, dob.unsafeGetBufferAndFlip(), hosts);
flusher.asyncFlush(alloc);
}
catch (Throwable e)
catch (IOException e)
{
callbacks.onWriteFailed(id, record, writeContext, e);
// exception during record serialization into the scratch buffer
throw new RuntimeException(e);
}
return recordPointer;
}
private ActiveSegment<K, V>.Allocation allocate(int entrySize, Set<Integer> hosts)
@ -756,6 +854,17 @@ public class Journal<K, V> implements Shutdownable
segment.forEachRecord(consumer);
}
@VisibleForTesting
public void closeCurrentSegmentForTesting()
{
ActiveSegment<K, V> segment = currentSegment;
advanceSegment(segment);
while (!segments().isSwitched(segment))
{
LockSupport.parkNanos(1000);
}
}
/*
* Static helper methods used by journal components
*/
@ -815,4 +924,11 @@ public class Journal<K, V> implements Shutdownable
"Check %s to see if not enough free space is the reason for this error.",
message, segmentSize, availableDiskSpace, directory);
}
@VisibleForTesting
public void truncateForTesting()
{
advanceSegment(null);
segments.set(Segments.none());
}
}

View File

@ -241,6 +241,36 @@ final class OnDiskIndex<K> extends Index<K>
return keyIndex < 0 ? -1 : offsetAtIndex(keyIndex);
}
@Override
public int[] lookUpAll(K id)
{
if (!mayContainId(id))
return new int[0];
int start = binarySearch(id);
int firstKeyIndex = start;
for (int i = firstKeyIndex - 1; i >= 0 && id.equals(keyAtIndex(i)); i--)
firstKeyIndex = i;
if (firstKeyIndex < 0)
return new int[0];
int lastKeyIndex = start;
for (int i = lastKeyIndex + 1; i < entryCount && id.equals(keyAtIndex(i)); i++)
lastKeyIndex = i;
int[] all = new int[lastKeyIndex - firstKeyIndex + 1];
int idx = firstKeyIndex;
for (int i = 0; i < all.length; i++)
{
all[i] = offsetAtIndex(idx);
idx++;
}
return all;
}
private K keyAtIndex(int index)
{
return keySupport.deserialize(buffer, FILE_PREFIX_SIZE + index * ENTRY_SIZE, descriptor.userVersion);

View File

@ -24,5 +24,5 @@ import org.agrona.collections.IntHashSet;
@FunctionalInterface
public interface RecordConsumer<K>
{
void accept(K key, ByteBuffer buffer, IntHashSet hosts, int userVersion);
void accept(long segment, int position, K key, ByteBuffer buffer, IntHashSet hosts, int userVersion);
}

View File

@ -46,6 +46,7 @@ abstract class Segment<K, V> implements Closeable, RefCounted<Segment<K, V>>
abstract Index<K> index();
abstract boolean isActive();
abstract boolean isFlushed(long position);
boolean isStatic() { return !isActive(); }
abstract ActiveSegment<K, V> asActive();
@ -65,7 +66,7 @@ abstract class Segment<K, V> implements Closeable, RefCounted<Segment<K, V>>
if (read(offset, into))
{
Invariants.checkState(id.equals(into.key), "Index for %s read incorrect key: expected %s but read %s", descriptor, id, into.key);
consumer.accept(id, into.value, into.hosts, descriptor.userVersion);
consumer.accept(descriptor.timestamp, offset, id, into.value, into.hosts, descriptor.userVersion);
return true;
}
return false;
@ -80,5 +81,16 @@ abstract class Segment<K, V> implements Closeable, RefCounted<Segment<K, V>>
return true;
}
void readAll(K id, EntrySerializer.EntryHolder<K> into, Runnable onEntry)
{
int[] all = index().lookUpAll(id);
for (int i = 0; i < all.length; i++)
{
Invariants.checkState(read(all[i], into), "Read should always return true");
onEntry.run();
}
}
abstract boolean read(int offset, EntrySerializer.EntryHolder<K> into);
}

View File

@ -98,6 +98,15 @@ class Segments<K, V>
into.add(segment.asActive());
}
boolean isSwitched(ActiveSegment<K, V> active)
{
for (Segment<K, V> segment : segments.values())
if (!segment.isActive() && active.descriptor.equals(segment.descriptor))
return true;
return false;
}
ActiveSegment<K, V> oldestActive()
{
Segment<K, V> oldest = null;
@ -171,6 +180,14 @@ class Segments<K, V>
}
}
boolean isFlushed(RecordPointer recordPointer)
{
Segment<K, V> segment = segments.get(recordPointer.segment);
if (null == segment)
throw new IllegalArgumentException("Can not reference segment " + recordPointer.segment);
return segment.isFlushed(recordPointer.position);
}
ReferencedSegment<K, V> selectAndReference(long segmentTimestamp)
{
Segment<K, V> segment = segments.get(segmentTimestamp);

View File

@ -182,6 +182,12 @@ final class StaticSegment<K, V> extends Segment<K, V>
return false;
}
@Override
boolean isFlushed(long position)
{
return true;
}
@Override
ActiveSegment<K, V> asActive()
{
@ -221,7 +227,7 @@ final class StaticSegment<K, V> extends Segment<K, V>
{
while (reader.advance())
{
consumer.accept(reader.id(), reader.record(), reader.hosts(), descriptor.userVersion);
consumer.accept(descriptor.timestamp, reader.offset(), reader.id(), reader.record(), reader.hosts(), descriptor.userVersion);
}
}
}

View File

@ -24,6 +24,7 @@ import org.apache.cassandra.io.util.DataOutputPlus;
public interface ValueSerializer<K, V>
{
// TODO (required): this is completely unused in Journal
int serializedSize(K key, V value, int userVersion);
void serialize(K key, V value, DataOutputPlus out, int userVersion) throws IOException;

View File

@ -18,7 +18,9 @@
package org.apache.cassandra.service.accord;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
@ -47,8 +49,7 @@ import accord.local.NodeTimeService;
import accord.local.PreLoadContext;
import accord.local.RedundantBefore;
import accord.local.SafeCommandStore;
import accord.local.SerializerSupport.MessageProvider;
import accord.messages.Message;
import accord.primitives.Keys;
import accord.primitives.Ranges;
import accord.primitives.RoutableKey;
import accord.primitives.Timestamp;
@ -66,7 +67,6 @@ import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.metrics.AccordStateCacheMetrics;
import org.apache.cassandra.service.accord.api.PartitionKey;
import org.apache.cassandra.service.accord.async.AsyncOperation;
import org.apache.cassandra.service.accord.async.ExecutionOrder;
import org.apache.cassandra.service.accord.events.CacheEvents;
import org.apache.cassandra.utils.Clock;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
@ -76,7 +76,6 @@ import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFac
public class AccordCommandStore extends CommandStore implements CacheSize
{
private static final Logger logger = LoggerFactory.getLogger(AccordCommandStore.class);
private static final boolean CHECK_THREADS = CassandraRelevantProperties.TEST_ACCORD_STORE_THREAD_CHECKS_ENABLED.getBoolean();
private static long getThreadId(ExecutorService executor)
@ -101,7 +100,6 @@ public class AccordCommandStore extends CommandStore implements CacheSize
public final String loggingId;
private final IJournal journal;
private final ExecutorService executor;
private final ExecutionOrder executionOrder;
private final AccordStateCache stateCache;
private final AccordStateCache.Instance<TxnId, Command, AccordSafeCommand> commandCache;
private final AccordStateCache.Instance<Key, TimestampsForKey, AccordSafeTimestampsForKey> timestampsForKeyCache;
@ -206,7 +204,6 @@ public class AccordCommandStore extends CommandStore implements CacheSize
this.journal = journal;
loggingId = String.format("[%s]", id);
executor = executorFactory().sequential(CommandStore.class.getSimpleName() + '[' + id + ']');
executionOrder = new ExecutionOrder();
threadId = getThreadId(executor);
stateCache = new AccordStateCache(loadExecutor, saveExecutor, 8 << 20, cacheMetrics);
commandCache =
@ -214,7 +211,7 @@ public class AccordCommandStore extends CommandStore implements CacheSize
AccordSafeCommand.class,
AccordSafeCommand::new,
this::loadCommand,
this::saveCommand,
this::appendToKeyspace,
this::validateCommand,
AccordObjectSizes::command);
registerJfrListener(id, commandCache, "Command");
@ -333,22 +330,33 @@ public class AccordCommandStore extends CommandStore implements CacheSize
{
return commandsForKeyCache;
}
Command loadCommand(TxnId txnId)
@Nullable
@VisibleForTesting
public Runnable appendToKeyspace(Command before, Command after)
{
return AccordKeyspace.loadCommand(this, txnId);
if (after.keysOrRanges() != null && after.keysOrRanges() instanceof Keys)
return null;
Mutation mutation = AccordKeyspace.getCommandMutation(this.id, before, after, nextSystemTimestampMicros());
// TODO (required): make sure we test recovering when this has failed to be persisted
if (null != mutation)
return mutation::applyUnsafe;
return null;
}
@Nullable
Runnable saveCommand(Command before, Command after)
@VisibleForTesting
public void appendToLog(Command before, Command after, Runnable runnable)
{
Mutation mutation = AccordKeyspace.getCommandMutation(id, before, after, nextSystemTimestampMicros());
// TODO (required): make sure we test recovering when this has failed to be persisted
return null != mutation ? mutation::applyUnsafe : null;
journal.appendCommand(id, Collections.singletonList(SavedCommand.SavedDiff.diff(before, after)), null, runnable);
}
boolean validateCommand(TxnId txnId, Command evicting)
{
Command reloaded = AccordKeyspace.unsafeLoadCommand(this, txnId);
Command reloaded = loadCommand(txnId);
return (evicting == null && reloaded == null) || (evicting != null && reloaded != null && reloaded.isEqualOrFuller(evicting));
}
@ -450,11 +458,6 @@ public class AccordCommandStore extends CommandStore implements CacheSize
return progressLog;
}
public ExecutionOrder executionOrder()
{
return executionOrder;
}
@Override
public AsyncChain<Void> execute(PreLoadContext preLoadContext, Consumer<? super SafeCommandStore> consumer)
{
@ -557,14 +560,14 @@ public class AccordCommandStore extends CommandStore implements CacheSize
public NavigableMap<TxnId, Ranges> bootstrapBeganAt() { return super.bootstrapBeganAt(); }
public NavigableMap<Timestamp, Ranges> safeToRead() { return super.safeToRead(); }
MessageProvider makeMessageProvider(TxnId txnId)
public void appendCommands(List<SavedCommand.SavedDiff> commands, List<Command> sanityCheck, Runnable onFlush)
{
return journal.makeMessageProvider(txnId);
journal.appendCommand(id, commands, sanityCheck, onFlush);
}
@VisibleForTesting
public void appendToJournal(Message message)
public Command loadCommand(TxnId txnId)
{
journal.appendMessageBlocking(message);
return journal.loadCommand(id, txnId);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Set;
@ -52,20 +51,14 @@ import accord.api.Key;
import accord.local.CommandsForKey;
import accord.impl.TimestampsForKey;
import accord.local.Command;
import accord.local.Command.WaitingOn;
import accord.local.CommandStore;
import accord.local.CommonAttributes;
import accord.local.DurableBefore;
import accord.local.Listeners;
import accord.local.Node;
import accord.local.RedundantBefore;
import accord.local.SaveStatus;
import accord.local.SerializerSupport;
import accord.local.SerializerSupport.MessageProvider;
import accord.local.SerializerSupport.WaitingOnProvider;
import accord.local.Status;
import accord.local.Status.Durability;
import accord.primitives.Ballot;
import accord.primitives.Ranges;
import accord.primitives.Routable;
import accord.primitives.Route;
@ -115,7 +108,6 @@ import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.rows.BTreeRow;
import org.apache.cassandra.db.rows.BufferCell;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.CellPath;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.db.rows.Row.Deletion;
import org.apache.cassandra.db.rows.RowIterator;
@ -156,10 +148,8 @@ import org.apache.cassandra.service.accord.serializers.CommandsForKeySerializer;
import org.apache.cassandra.service.accord.serializers.KeySerializers;
import org.apache.cassandra.service.accord.serializers.ListenerSerializers;
import org.apache.cassandra.service.accord.serializers.TopologySerializers;
import org.apache.cassandra.service.accord.serializers.WaitingOnSerializer;
import org.apache.cassandra.transport.Dispatcher;
import org.apache.cassandra.utils.Clock.Global;
import org.apache.cassandra.utils.Throwables;
import org.apache.cassandra.utils.btree.BTree;
import org.apache.cassandra.utils.btree.BTreeSet;
import org.apache.cassandra.utils.bytecomparable.ByteComparable;
@ -248,11 +238,6 @@ public class AccordKeyspace
+ "route blob,"
+ "durability int,"
+ format("execute_at %s,", TIMESTAMP_TUPLE)
+ format("promised_ballot %s,", TIMESTAMP_TUPLE)
+ format("accepted_ballot %s,", TIMESTAMP_TUPLE)
+ format("execute_atleast %s,", TIMESTAMP_TUPLE)
+ "waiting_on blob,"
+ "listeners set<blob>, "
+ "PRIMARY KEY((store_id, domain, txn_id))"
+ ')')
.partitioner(new LocalPartitioner(CompositeType.getInstance(Int32Type.instance, Int32Type.instance, TIMESTAMP_TYPE)))
@ -297,11 +282,6 @@ public class AccordKeyspace
public static final ColumnMetadata route = getColumn(Commands, "route");
public static final ColumnMetadata durability = getColumn(Commands, "durability");
public static final ColumnMetadata execute_at = getColumn(Commands, "execute_at");
static final ColumnMetadata promised_ballot = getColumn(Commands, "promised_ballot");
static final ColumnMetadata accepted_ballot = getColumn(Commands, "accepted_ballot");
static final ColumnMetadata execute_atleast = getColumn(Commands, "execute_atleast");
static final ColumnMetadata waiting_on = getColumn(Commands, "waiting_on");
static final ColumnMetadata listeners = getColumn(Commands, "listeners");
public static final ColumnMetadata[] TRUNCATE_FIELDS = new ColumnMetadata[] { durability, execute_at, route, status };
@ -779,65 +759,6 @@ public class AccordKeyspace
addCellIfModified(column, get, v -> accessor.valueOf(v.ordinal()), builder, timestampMicros, nowInSeconds, original, command);
}
private static <C, V> void addSetChanges(ColumnMetadata column, Function<C, Set<V>> get, SerializeFunction<V> serialize, Row.Builder builder, long timestampMicros, int nowInSec, C original, C command) throws IOException
{
Set<V> prev = original != null ? get.apply(original) : Collections.emptySet();
if (prev == null) prev = Collections.emptySet();
Set<V> value = get.apply(command);
if (value == null) value = Collections.emptySet();
if (value.isEmpty() && !prev.isEmpty())
{
builder.addComplexDeletion(column, DeletionTime.build(timestampMicros, nowInSec));
return;
}
for (V item : Sets.difference(value, prev))
builder.addCell(live(column, timestampMicros, EMPTY_BYTE_BUFFER, CellPath.create(serialize.apply(item))));
for (V item : Sets.difference(prev, value))
builder.addCell(tombstone(column, timestampMicros, nowInSec, CellPath.create(serialize.apply(item))));
}
private static <C, K, V> void addMapChanges(ColumnMetadata column, Function<C, Map<K, V>> get, SerializeFunction<K> serializeKey, SerializeFunction<V> serializeVal, Row.Builder builder, long timestampMicros, int nowInSec, C original, C command) throws IOException
{
Map<K, V> prev = original != null ? get.apply(original) : Collections.emptyMap();
if (prev == null) prev = Collections.emptyMap();
Map<K, V> value = get.apply(command);
if (value == null) value = Collections.emptyMap();
if (value.isEmpty() && !prev.isEmpty())
{
builder.addComplexDeletion(column, DeletionTime.build(timestampMicros, nowInSec));
return;
}
for (Map.Entry<K, V> entry : value.entrySet())
{
K key = entry.getKey();
V pVal = prev.get(key);
if (pVal != null && pVal.equals(entry.getValue()))
continue;
builder.addCell(live(column, timestampMicros, serializeVal.apply(entry.getValue()), CellPath.create(serializeKey.apply(key))));
}
for (K key : Sets.difference(prev.keySet(), value.keySet()))
builder.addCell(tombstone(column, timestampMicros, nowInSec, CellPath.create(serializeKey.apply(key))));
}
private static <K, V> int estimateMapChanges(Map<K, V> prev, Map<K, V> value)
{
return Math.abs(prev.size() - value.size());
}
private static <C, K, V> int estimateMapChanges(Function<C, Map<K, V>> get, C original, C command)
{
Map<K, V> prev = original != null ? get.apply(original) : Collections.emptyMap();
if (prev == null) prev = Collections.emptyMap();
Map<K, V> value = get.apply(command);
if (value == null) value = Collections.emptyMap();
return estimateMapChanges(prev, value);
}
public static Mutation getCommandMutation(AccordCommandStore commandStore, AccordSafeCommand liveCommand, long timestampMicros)
{
return getCommandMutation(commandStore.id(), liveCommand.original(), liveCommand.current(), timestampMicros);
@ -856,21 +777,8 @@ public class AccordKeyspace
addEnumCellIfModified(CommandsColumns.durability, Command::durability, builder, timestampMicros, nowInSeconds, original, command);
addCellIfModified(CommandsColumns.route, Command::route, LocalVersionedSerializers.route, builder, timestampMicros, nowInSeconds, original, command);
addSetChanges(CommandsColumns.listeners, Command::durableListeners, v -> serialize(v, LocalVersionedSerializers.listeners), builder, timestampMicros, nowInSeconds, original, command);
addEnumCellIfModified(CommandsColumns.status, Command::saveStatus, builder, timestampMicros, nowInSeconds, original, command);
addCellIfModified(CommandsColumns.execute_at, Command::executeAt, AccordKeyspace::serializeTimestamp, builder, timestampMicros, nowInSeconds, original, command);
addCellIfModified(CommandsColumns.promised_ballot, Command::promised, AccordKeyspace::serializeTimestamp, builder, timestampMicros, nowInSeconds, original, command);
addCellIfModified(CommandsColumns.accepted_ballot, Command::acceptedOrCommitted, AccordKeyspace::serializeTimestamp, builder, timestampMicros, nowInSeconds, original, command);
if (command.txnId().kind().awaitsOnlyDeps())
addCellIfModified(CommandsColumns.execute_atleast, Command::executesAtLeast, AccordKeyspace::serializeTimestamp, builder, timestampMicros, nowInSeconds, original, command);
if (command.isStable() && !command.isTruncated())
{
Command.Committed committed = command.asCommitted();
Command.Committed originalCommitted = original != null && original.isCommitted() ? original.asCommitted() : null;
if (originalCommitted == null || committed.waitingOn != originalCommitted.waitingOn)
builder.addCell(live(CommandsColumns.waiting_on, timestampMicros, WaitingOnSerializer.serialize(committed.txnId(), committed.waitingOn)));
}
Row row = builder.build();
if (row.columnCount() == 0)
@ -966,23 +874,6 @@ public class AccordKeyspace
return deserializeTimestampOrDefault(row.getBlob(name), ByteBufferAccessor.instance, factory, defaultVal);
}
private static ByteBuffer bytesOrNull(Row row, ColumnMetadata column)
{
Cell<?> cell = row.getCell(column);
return cell != null && !cell.isTombstone() ? cell.buffer() : null;
}
private static <T extends Timestamp> T deserializeTimestampOrDefault(Row row, ColumnMetadata column, TimestampFactory<T> factory, T valIfNull)
{
ByteBuffer bytes = bytesOrNull(row, column);
if (bytes == null)
return valIfNull;
T result = deserializeTimestampOrNull(bytes, factory);
if (result == null)
return valIfNull;
return result;
}
public static Durability deserializeDurabilityOrNull(Cell cell)
{
return cell == null ? null : CommandSerializers.durability.forOrdinal(cell.accessor().getInt(cell.value(), 0));
@ -993,6 +884,7 @@ public class AccordKeyspace
return cell == null ? null : CommandSerializers.saveStatus.forOrdinal(cell.accessor().getInt(cell.value(), 0));
}
@VisibleForTesting
public static UntypedResultSet loadCommandRow(CommandStore commandStore, TxnId txnId)
{
String cql = "SELECT * FROM " + ACCORD_KEYSPACE_NAME + '.' + COMMANDS + ' ' +
@ -1006,12 +898,6 @@ public class AccordKeyspace
txnId.msb, txnId.lsb, txnId.node.id);
}
public static void findAllCommandsByDomain(int commandStore, Routable.Domain domain, Set<String> columns, Observable<UntypedResultSet.Row> callback)
{
WalkCommandsForDomain work = new WalkCommandsForDomain(commandStore, domain, columns, Stage.READ.executor(), callback);
work.schedule();
}
private static abstract class TableWalk implements Runnable, DebuggableTask
{
private final long creationTimeNanos = Global.nanoTime();
@ -1209,47 +1095,6 @@ public class AccordKeyspace
}
}
public static Command loadCommand(AccordCommandStore commandStore, TxnId txnId)
{
commandStore.checkNotInStoreThread();
return unsafeLoadCommand(commandStore, txnId);
}
static Command unsafeLoadCommand(AccordCommandStore commandStore, TxnId txnId)
{
UntypedResultSet rows = loadCommandRow(commandStore, txnId);
if (rows.isEmpty())
return null;
UntypedResultSet.Row row = rows.one();
try
{
checkState(deserializeTxnId(row).equals(txnId));
CommonAttributes.Mutable attrs =
new CommonAttributes.Mutable(txnId)
.durability(deserializeDurability(row))
.route(deserializeRouteOrNull(row))
.setListeners(deserializeListeners(row));
SaveStatus status = deserializeStatus(row);
Timestamp executeAt = deserializeExecuteAtOrNull(row);
Ballot promised = deserializePromisedOrNull(row);
Ballot accepted = deserializeAcceptedOrNull(row);
Timestamp executeAtLeast = status.is(Status.Truncated) && txnId.kind().awaitsOnlyDeps() ? deserializeExecuteAtLeastOrNull(row) : null;
WaitingOnProvider waitingOn = deserializeWaitingOn(txnId, row);
MessageProvider messages = commandStore.makeMessageProvider(txnId);
return SerializerSupport.reconstruct(commandStore.agent(), commandStore.unsafeRangesForEpoch(), attrs, status, executeAt, executeAtLeast, promised, accepted, waitingOn, messages);
}
catch (Throwable t)
{
logger.error("Exception loading AccordCommand " + txnId, t);
throw Throwables.unchecked(t);
}
}
public static TxnId deserializeTxnId(UntypedResultSet.Row row)
{
return deserializeTimestampOrNull(row, "txn_id", TxnId::fromBits);
@ -1303,55 +1148,6 @@ public class AccordKeyspace
return new Listeners.Immutable(result);
}
public static SaveStatus deserializeStatus(UntypedResultSet.Row row)
{
// TODO (performance, expected): something less brittle than ordinal, more efficient than values()
return SaveStatus.values()[row.getInt("status")];
}
public static Timestamp deserializeExecuteAtOrNull(UntypedResultSet.Row row)
{
return deserializeTimestampOrNull(row, "execute_at", Timestamp::fromBits);
}
public static Timestamp deserializeExecuteAtLeastOrNull(UntypedResultSet.Row row)
{
return deserializeTimestampOrNull(row, "execute_atleast", Timestamp::fromBits);
}
public static Ballot deserializePromisedOrNull(UntypedResultSet.Row row)
{
return deserializeTimestampOrNull(row.getBlob("promised_ballot"), Ballot::fromBits);
}
public static Ballot deserializeAcceptedOrNull(UntypedResultSet.Row row)
{
return deserializeTimestampOrNull(row.getBlob("accepted_ballot"), Ballot::fromBits);
}
private static WaitingOnProvider deserializeWaitingOn(TxnId txnId, UntypedResultSet.Row row)
{
ByteBuffer bytes = row.getBlob("waiting_on");
return (deps) ->
{
if (bytes == null)
return null;
if (!bytes.hasRemaining())
return WaitingOn.none(deps);
try
{
return WaitingOnSerializer.deserialize(txnId, deps.keyDeps.keys(), deps.rangeDeps.txnIds(), bytes);
}
catch (IOException e)
{
throw Throwables.unchecked(e);
}
};
}
public static PartitionKey deserializeKey(ByteBuffer buffer)
{
List<ByteBuffer> split = KEY_TYPE.unpack(buffer, ByteBufferAccessor.instance);

View File

@ -91,12 +91,18 @@ public class AccordSafeCommand extends SafeCommand implements AccordSafeState<Tx
this.current = command;
}
@Override
public Command original()
{
checkNotInvalidated();
return original;
}
public SavedCommand.SavedDiff diff()
{
return SavedCommand.diff(original, current);
}
@Override
public void preExecute()
{
@ -125,14 +131,14 @@ public class AccordSafeCommand extends SafeCommand implements AccordSafeState<Tx
}
@Override
public void addListener(Command.TransientListener listener)
public void addListener(TransientListener listener)
{
checkNotInvalidated();
global.addListener(listener);
}
@Override
public boolean removeListener(Command.TransientListener listener)
public boolean removeListener(TransientListener listener)
{
checkNotInvalidated();
return global.removeListener(listener);

View File

@ -33,10 +33,6 @@ import accord.impl.CommandsSummary;
import accord.local.CommandStores.RangesForEpoch;
import accord.local.NodeTimeService;
import accord.local.PreLoadContext;
import accord.messages.BeginRecovery;
import accord.messages.PreAccept;
import accord.messages.TxnRequest;
import accord.primitives.AbstractKeys;
import accord.primitives.AbstractRanges;
import accord.primitives.Deps;
@ -50,7 +46,6 @@ import accord.primitives.TxnId;
public class AccordSafeCommandStore extends AbstractSafeCommandStore<AccordSafeCommand, AccordSafeTimestampsForKey, AccordSafeCommandsForKey>
{
private final long preAcceptTimeout;
private final Map<TxnId, AccordSafeCommand> commands;
private final NavigableMap<Key, AccordSafeCommandsForKey> commandsForKeys;
private final NavigableMap<Key, AccordSafeTimestampsForKey> timestampsForKeys;
@ -59,7 +54,6 @@ public class AccordSafeCommandStore extends AbstractSafeCommandStore<AccordSafeC
private final RangesForEpoch ranges;
private AccordSafeCommandStore(PreLoadContext context,
long preAcceptTimeout,
Map<TxnId, AccordSafeCommand> commands,
NavigableMap<Key, AccordSafeTimestampsForKey> timestampsForKey,
NavigableMap<Key, AccordSafeCommandsForKey> commandsForKey,
@ -67,7 +61,6 @@ public class AccordSafeCommandStore extends AbstractSafeCommandStore<AccordSafeC
AccordCommandStore commandStore)
{
super(context);
this.preAcceptTimeout = preAcceptTimeout;
this.commands = commands;
this.timestampsForKeys = timestampsForKey;
this.commandsForKeys = commandsForKey;
@ -83,17 +76,7 @@ public class AccordSafeCommandStore extends AbstractSafeCommandStore<AccordSafeC
@Nullable AccordSafeCommandsForRanges commandsForRanges,
AccordCommandStore commandStore)
{
long preAcceptTimeoutMicros = -1;
if ((preLoadContext instanceof PreAccept || preLoadContext instanceof BeginRecovery))
{
TxnRequest<?> preAccept = (TxnRequest<?>) preLoadContext;
AccordJournal.RequestContext context = (AccordJournal.RequestContext) preAccept.replyContext();
// TODO (required): SimulatedDepsTest and some other tests aren't calling preProcess, hence do not set context
if (context != null)
preAcceptTimeoutMicros = context.preAcceptTimeout();
}
return new AccordSafeCommandStore(preLoadContext, preAcceptTimeoutMicros, commands, timestampsForKey, commandsForKey, commandsForRanges, commandStore);
return new AccordSafeCommandStore(preLoadContext, commands, timestampsForKey, commandsForKey, commandsForRanges, commandStore);
}
@Override
@ -187,15 +170,6 @@ public class AccordSafeCommandStore extends AbstractSafeCommandStore<AccordSafeC
return commandStore.time();
}
@Override
public long preAcceptTimeout()
{
if (preAcceptTimeout == -1)
return super.preAcceptTimeout();
return preAcceptTimeout;
}
@Override
public RangesForEpoch ranges()
{

View File

@ -611,7 +611,7 @@ public class AccordService implements IAccordService, Shutdownable
{
// currently, we only create LocalRequests that have side effects and need to be persisted
Invariants.checkState(request.type().hasSideEffects());
journal.appendLocalRequest(request, callback);
journal.processLocalRequest(request, callback);
}
private static RequestTimeoutException newTimeout(TxnId txnId, Txn txn, ConsistencyLevel consistencyLevel)
@ -699,6 +699,11 @@ public class AccordService implements IAccordService, Shutdownable
return node;
}
public AccordJournal journal()
{
return journal;
}
@Override
public Future<Void> epochReady(Epoch epoch)
{

View File

@ -52,7 +52,7 @@ public class AccordVerbHandler<T extends Request> implements IVerbHandler<T>
if (request.type().hasSideEffects())
{
journal.appendRemoteRequest(request, message);
journal.processRemoteRequest(request, message);
return;
}

View File

@ -18,12 +18,20 @@
package org.apache.cassandra.service.accord;
import accord.local.SerializerSupport;
import accord.messages.Message;
import java.util.List;
import accord.local.Command;
import accord.primitives.TxnId;
public interface IJournal
{
SerializerSupport.MessageProvider makeMessageProvider(TxnId txnId);
void appendMessageBlocking(Message message);
Command loadCommand(int commandStoreId, TxnId txnId);
/**
* Append outcomes to the log.
*
* Returns whether an async flush was requested. If it returns false, all commands are guaranteed to be flushed by that time.
* If it returns false, onFlush runnable will run whenever flush is done.
*/
void appendCommand(int commandStoreId, List<SavedCommand.SavedDiff> command, List<Command> sanityCheck, Runnable onFlush);
}

View File

@ -0,0 +1,247 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cassandra.service.accord;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Objects;
import java.util.zip.Checksum;
import accord.local.Node;
import accord.primitives.Timestamp;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.journal.KeySupport;
import org.apache.cassandra.utils.ByteArrayUtil;
import static org.apache.cassandra.db.TypeSizes.BYTE_SIZE;
import static org.apache.cassandra.db.TypeSizes.INT_SIZE;
import static org.apache.cassandra.db.TypeSizes.LONG_SIZE;
import static org.apache.cassandra.db.TypeSizes.SHORT_SIZE;
public final class JournalKey
{
final Timestamp timestamp;
final AccordJournal.Type type; // TODO (desired): do we even need type here anymore?
final int commandStoreId;
JournalKey(Timestamp timestamp, AccordJournal.Type type)
{
this(timestamp, type, -1);
}
JournalKey(Timestamp timestamp, AccordJournal.Type type, int commandStoreId)
{
if (timestamp == null) throw new NullPointerException("Null timestamp for type " + type);
this.timestamp = timestamp;
this.type = type;
this.commandStoreId = commandStoreId;
}
/**
* Support for (de)serializing and comparing record keys.
* <p>
* Implements its own serialization and comparison for {@link Timestamp} to satisty
* {@link KeySupport} contract - puts hybrid logical clock ahead of epoch
* when ordering timestamps. This is done for more precise elimination of candidate
* segments by min/max record key in segment.
*/
static final KeySupport<JournalKey> SUPPORT = new KeySupport<>()
{
private static final int HLC_OFFSET = 0;
private static final int EPOCH_AND_FLAGS_OFFSET = HLC_OFFSET + LONG_SIZE;
private static final int NODE_OFFSET = EPOCH_AND_FLAGS_OFFSET + LONG_SIZE;
private static final int TYPE_OFFSET = NODE_OFFSET + INT_SIZE;
private static final int CS_ID_OFFSET = TYPE_OFFSET + BYTE_SIZE;
@Override
public int serializedSize(int userVersion)
{
return LONG_SIZE // timestamp.hlc()
+ 6 // timestamp.epoch()
+ 2 // timestamp.flags()
+ INT_SIZE // timestamp.node
+ BYTE_SIZE // type
+ SHORT_SIZE; // commandStoreId
}
@Override
public void serialize(JournalKey key, DataOutputPlus out, int userVersion) throws IOException
{
serializeTimestamp(key.timestamp, out);
out.writeByte(key.type.id);
out.writeShort(key.commandStoreId);
}
private void serialize(JournalKey key, byte[] out)
{
serializeTimestamp(key.timestamp, out);
out[20] = (byte) (key.type.id & 0xFF);
ByteArrayUtil.putShort(out, 21, (short) key.commandStoreId);
}
@Override
public JournalKey deserialize(DataInputPlus in, int userVersion) throws IOException
{
Timestamp timestamp = deserializeTimestamp(in);
int type = in.readByte();
int commandStoreId = in.readShort();
return new JournalKey(timestamp, AccordJournal.Type.fromId(type), commandStoreId);
}
@Override
public JournalKey deserialize(ByteBuffer buffer, int position, int userVersion)
{
Timestamp timestamp = deserializeTimestamp(buffer, position);
int type = buffer.get(position + TYPE_OFFSET);
int commandStoreId = buffer.getShort(position + CS_ID_OFFSET);
return new JournalKey(timestamp, AccordJournal.Type.fromId(type), commandStoreId);
}
private void serializeTimestamp(Timestamp timestamp, DataOutputPlus out) throws IOException
{
out.writeLong(timestamp.hlc());
out.writeLong(epochAndFlags(timestamp));
out.writeInt(timestamp.node.id);
}
private Timestamp deserializeTimestamp(DataInputPlus in) throws IOException
{
long hlc = in.readLong();
long epochAndFlags = in.readLong();
int nodeId = in.readInt();
return Timestamp.fromValues(epoch(epochAndFlags), hlc, flags(epochAndFlags), new Node.Id(nodeId));
}
private void serializeTimestamp(Timestamp timestamp, byte[] out)
{
ByteArrayUtil.putLong(out, 0, timestamp.hlc());
ByteArrayUtil.putLong(out, 8, epochAndFlags(timestamp));
ByteArrayUtil.putInt(out, 16, timestamp.node.id);
}
private Timestamp deserializeTimestamp(ByteBuffer buffer, int position)
{
long hlc = buffer.getLong(position + HLC_OFFSET);
long epochAndFlags = buffer.getLong(position + EPOCH_AND_FLAGS_OFFSET);
int nodeId = buffer.getInt(position + NODE_OFFSET);
return Timestamp.fromValues(epoch(epochAndFlags), hlc, flags(epochAndFlags), new Node.Id(nodeId));
}
@Override
public void updateChecksum(Checksum crc, JournalKey key, int userVersion)
{
byte[] out = AccordJournal.keyCRCBytes.get();
serialize(key, out);
crc.update(out, 0, out.length);
}
@Override
public int compareWithKeyAt(JournalKey k, ByteBuffer buffer, int position, int userVersion)
{
int cmp = compareWithTimestampAt(k.timestamp, buffer, position);
if (cmp != 0) return cmp;
byte type = buffer.get(position + TYPE_OFFSET);
cmp = Byte.compare((byte) k.type.id, type);
if (cmp != 0) return cmp;
short commandStoreId = buffer.getShort(position + CS_ID_OFFSET);
cmp = Short.compare((byte) k.commandStoreId, commandStoreId);
return cmp;
}
private int compareWithTimestampAt(Timestamp timestamp, ByteBuffer buffer, int position)
{
long hlc = buffer.getLong(position + HLC_OFFSET);
int cmp = Long.compareUnsigned(timestamp.hlc(), hlc);
if (cmp != 0) return cmp;
long epochAndFlags = buffer.getLong(position + EPOCH_AND_FLAGS_OFFSET);
cmp = Long.compareUnsigned(epochAndFlags(timestamp), epochAndFlags);
if (cmp != 0) return cmp;
int nodeId = buffer.getInt(position + NODE_OFFSET);
cmp = Integer.compareUnsigned(timestamp.node.id, nodeId);
return cmp;
}
@Override
public int compare(JournalKey k1, JournalKey k2)
{
int cmp = compare(k1.timestamp, k2.timestamp);
if (cmp == 0) cmp = Byte.compare((byte) k1.type.id, (byte) k2.type.id);
if (cmp == 0) cmp = Short.compare((short) k1.commandStoreId, (short) k2.commandStoreId);
return cmp;
}
private int compare(Timestamp timestamp1, Timestamp timestamp2)
{
int cmp = Long.compareUnsigned(timestamp1.hlc(), timestamp2.hlc());
if (cmp == 0) cmp = Long.compareUnsigned(epochAndFlags(timestamp1), epochAndFlags(timestamp2));
if (cmp == 0) cmp = Integer.compareUnsigned(timestamp1.node.id, timestamp2.node.id);
return cmp;
}
private long epochAndFlags(Timestamp timestamp)
{
return (timestamp.epoch() << 16) | (long) timestamp.flags();
}
private long epoch(long epochAndFlags)
{
return epochAndFlags >>> 16;
}
private int flags(long epochAndFlags)
{
return (int) (epochAndFlags & ((1 << 16) - 1));
}
};
@Override
public boolean equals(Object other)
{
if (this == other)
return true;
return (other instanceof JournalKey) && equals((JournalKey) other);
}
boolean equals(JournalKey other)
{
return this.type == other.type &&
this.timestamp.equals(other.timestamp) &&
this.commandStoreId == other.commandStoreId;
}
@Override
public int hashCode()
{
return Objects.hash(timestamp, type, commandStoreId);
}
public String toString()
{
return "Key{" +
"timestamp=" + timestamp +
", type=" + type +
", commandStoreId=" + commandStoreId +
'}';
}
}

View File

@ -0,0 +1,612 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cassandra.service.accord;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.function.Function;
import com.google.common.annotations.VisibleForTesting;
import accord.api.Result;
import accord.local.Command;
import accord.local.CommonAttributes;
import accord.local.Listeners;
import accord.local.SaveStatus;
import accord.local.Status;
import accord.primitives.Ballot;
import accord.primitives.PartialDeps;
import accord.primitives.PartialTxn;
import accord.primitives.Route;
import accord.primitives.Seekables;
import accord.primitives.Timestamp;
import accord.primitives.TxnId;
import accord.primitives.Writes;
import accord.utils.Invariants;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.journal.ValueSerializer;
import org.apache.cassandra.service.accord.serializers.CommandSerializers;
import org.apache.cassandra.service.accord.serializers.DepsSerializer;
import org.apache.cassandra.service.accord.serializers.KeySerializers;
import org.apache.cassandra.service.accord.serializers.WaitingOnSerializer;
import org.apache.cassandra.utils.Throwables;
import static org.apache.cassandra.db.TypeSizes.SHORT_SIZE;
public class SavedCommand
{
public static final ValueSerializer<JournalKey, Object> serializer = new SavedCommandSerializer();
// This enum is order-dependent
private enum HasFields
{
TXN_ID,
EXECUTE_AT,
SAVE_STATUS,
DURABILITY,
ACCEPTED,
PROMISED,
ROUTE,
PARTIAL_TXN,
PARTIAL_DEPS,
ADDITIONAL_KEYS,
WAITING_ON,
WRITES,
LISTENERS
}
public final TxnId txnId;
public final Timestamp executeAt;
public final SaveStatus saveStatus;
public final Status.Durability durability;
public final Ballot acceptedOrCommitted;
public final Ballot promised;
public final Route<?> route;
public final PartialTxn partialTxn;
public final PartialDeps partialDeps;
public final Seekables<?, ?> additionalKeysOrRanges;
public final Writes writes;
public final Listeners.Immutable<Command.DurableAndIdempotentListener> listeners;
public SavedCommand(TxnId txnId,
Timestamp executeAt,
SaveStatus saveStatus,
Status.Durability durability,
Ballot acceptedOrCommitted,
Ballot promised,
Route<?> route,
PartialTxn partialTxn,
PartialDeps partialDeps,
Seekables<?, ?> additionalKeysOrRanges,
Writes writes,
Listeners.Immutable<Command.DurableAndIdempotentListener> listeners)
{
this.txnId = txnId;
this.executeAt = executeAt;
this.saveStatus = saveStatus;
this.durability = durability;
this.acceptedOrCommitted = acceptedOrCommitted;
this.promised = promised;
this.route = route;
this.partialTxn = partialTxn;
this.partialDeps = partialDeps;
this.additionalKeysOrRanges = additionalKeysOrRanges;
this.writes = writes;
this.listeners = listeners;
}
public static SavedDiff diff(Command before, Command after)
{
if (before == after)
return null;
// TODO: we do not need to save `waitingOn` _every_ time.
Command.WaitingOn waitingOn = getWaitingOn(after);
return new SavedDiff(after.txnId(),
ifNotEqual(before, after, Command::executeAt, true),
ifNotEqual(before, after, Command::saveStatus, false),
ifNotEqual(before, after, Command::durability, false),
ifNotEqual(before, after, Command::acceptedOrCommitted, false),
ifNotEqual(before, after, Command::promised, false),
ifNotEqual(before, after, Command::route, true),
ifNotEqual(before, after, Command::partialTxn, false),
ifNotEqual(before, after, Command::partialDeps, false),
ifNotEqual(before, after, Command::additionalKeysOrRanges, false),
waitingOn,
ifNotEqual(before, after, Command::writes, false),
ifNotEqual(before, after, Command::durableListeners, true));
}
static Command reconstructFromDiff(List<LoadedDiff> diffs)
{
return reconstructFromDiff(diffs, CommandSerializers.APPLIED);
}
/**
* @param result is exposed because we are _not_ persisting result, since during loading or replay
* we do not expect we will have to send a result to the client, and data results
* can potentially contain a large number of entries, so it's best if they are not
* written into the log.
*/
@VisibleForTesting
static Command reconstructFromDiff(List<LoadedDiff> diffs, Result result)
{
TxnId txnId = null;
Timestamp executeAt = null;
SaveStatus saveStatus = null;
Status.Durability durability = null;
Ballot acceptedOrCommitted = Ballot.ZERO;
Ballot promised = null;
Route<?> route = null;
PartialTxn partialTxn = null;
PartialDeps partialDeps = null;
Seekables<?, ?> additionalKeysOrRanges = null;
WaitingOnProvider waitingOnProvider = null;
Writes writes = null;
Listeners.Immutable listeners = null;
for (LoadedDiff diff : diffs)
{
if (diff.txnId != null)
txnId = diff.txnId;
if (diff.executeAt != null)
executeAt = diff.executeAt;
if (diff.saveStatus != null)
saveStatus = diff.saveStatus;
if (diff.durability != null)
durability = diff.durability;
if (diff.acceptedOrCommitted != null)
acceptedOrCommitted = diff.acceptedOrCommitted;
if (diff.promised != null)
promised = diff.promised;
if (diff.route != null)
route = diff.route;
if (diff.partialTxn != null)
partialTxn = diff.partialTxn;
if (diff.partialDeps != null)
partialDeps = diff.partialDeps;
if (diff.additionalKeysOrRanges != null)
additionalKeysOrRanges = diff.additionalKeysOrRanges;
if (diff.waitingOn != null)
waitingOnProvider = diff.waitingOn;
if (diff.writes != null)
writes = diff.writes;
if (diff.listeners != null)
listeners = diff.listeners;
}
CommonAttributes.Mutable attrs = new CommonAttributes.Mutable(txnId);
if (partialTxn != null)
attrs.partialTxn(partialTxn);
if (durability != null)
attrs.durability(durability);
if (route != null)
attrs.route(route);
if (partialDeps != null &&
(saveStatus.known.deps != Status.KnownDeps.NoDeps &&
saveStatus.known.deps != Status.KnownDeps.DepsErased &&
saveStatus.known.deps != Status.KnownDeps.DepsUnknown))
attrs.partialDeps(partialDeps);
if (additionalKeysOrRanges != null)
attrs.additionalKeysOrRanges(additionalKeysOrRanges);
if (listeners != null && !listeners.isEmpty())
attrs.setListeners(listeners);
Command.WaitingOn waitingOn = null;
if (waitingOnProvider != null)
waitingOn = waitingOnProvider.provide(txnId, partialDeps);
Invariants.checkState(saveStatus != null,
"Save status is null after applying %s", diffs);
switch (saveStatus.status)
{
case NotDefined:
return saveStatus == SaveStatus.Uninitialised ? Command.NotDefined.uninitialised(attrs.txnId())
: Command.NotDefined.notDefined(attrs, promised);
case PreAccepted:
return Command.PreAccepted.preAccepted(attrs, executeAt, promised);
case AcceptedInvalidate:
case Accepted:
case PreCommitted:
return Command.Accepted.accepted(attrs, saveStatus, executeAt, promised, acceptedOrCommitted);
case Committed:
case Stable:
return Command.Committed.committed(attrs, saveStatus, executeAt, promised, acceptedOrCommitted, waitingOn);
case PreApplied:
case Applied:
return Command.Executed.executed(attrs, saveStatus, executeAt, promised, acceptedOrCommitted, waitingOn, writes, result);
case Truncated:
case Invalidated:
default:
throw new IllegalStateException();
}
}
// TODO (required): this convert function was added only because AsyncOperationTest was failing without it; maybe after switching to loading from the log we can just pass l and r directly or remove != null checks.
private static <OBJ, VAL> VAL ifNotEqual(OBJ lo, OBJ ro, Function<OBJ, VAL> convert, boolean allowClassMismatch)
{
VAL l = null;
VAL r = null;
if (lo != null) l = convert.apply(lo);
if (ro != null) r = convert.apply(ro);
if (l == r)
return null;
if (l == null || r == null)
return r;
assert allowClassMismatch || l.getClass() == r.getClass() : String.format("%s != %s", l.getClass(), r.getClass());
if (l.equals(r))
return null;
return r;
}
static Command.WaitingOn getWaitingOn(Command command)
{
if (command instanceof Command.Committed)
return command.asCommitted().waitingOn();
return null;
}
public static class SavedDiff extends SavedCommand
{
public final Command.WaitingOn waitingOn;
public SavedDiff(TxnId txnId,
Timestamp executeAt,
SaveStatus saveStatus,
Status.Durability durability,
Ballot acceptedOrCommitted,
Ballot promised,
Route<?> route,
PartialTxn partialTxn,
PartialDeps partialDeps,
Seekables<?, ?> additionalKeysOrRanges,
Command.WaitingOn waitingOn,
Writes writes,
Listeners.Immutable<Command.DurableAndIdempotentListener> listeners)
{
super(txnId, executeAt, saveStatus, durability, acceptedOrCommitted, promised, route, partialTxn, partialDeps, additionalKeysOrRanges, writes, listeners);
this.waitingOn = waitingOn;
}
@Override
public String toString()
{
return "SavedDiff{" +
" txnId=" + txnId +
", executeAt=" + executeAt +
", saveStatus=" + saveStatus +
", durability=" + durability +
", acceptedOrCommitted=" + acceptedOrCommitted +
", promised=" + promised +
", route=" + route +
", partialTxn=" + partialTxn +
", partialDeps=" + partialDeps +
", writes=" + writes +
", waitingOn=" + waitingOn +
'}';
}
}
public static class LoadedDiff extends SavedCommand
{
public final WaitingOnProvider waitingOn;
public LoadedDiff(TxnId txnId,
Timestamp executeAt,
SaveStatus saveStatus,
Status.Durability durability,
Ballot acceptedOrCommitted,
Ballot promised,
Route<?> route,
PartialTxn partialTxn,
PartialDeps partialDeps,
Seekables<?, ?> additionalKeysOrRanges,
WaitingOnProvider waitingOn,
Writes writes,
Listeners.Immutable<Command.DurableAndIdempotentListener> listeners)
{
super(txnId, executeAt, saveStatus, durability, acceptedOrCommitted, promised, route, partialTxn, partialDeps, additionalKeysOrRanges, writes, listeners);
this.waitingOn = waitingOn;
}
public String toString()
{
return "LoadedDiff{" +
"waitingOn=" + waitingOn +
'}';
}
}
final static class SavedCommandSerializer implements ValueSerializer<JournalKey, Object>
{
@Override
public int serializedSize(JournalKey key, Object value, int userVersion)
{
SavedDiff diff = (SavedDiff) value;
long size = 0;
size += SHORT_SIZE; // flags
if (diff.txnId != null)
size += CommandSerializers.txnId.serializedSize(diff.txnId, userVersion);
if (diff.executeAt != null)
size += CommandSerializers.timestamp.serializedSize(diff.executeAt, userVersion);
if (diff.saveStatus != null)
size += Integer.BYTES;
if (diff.durability != null)
size += Integer.BYTES;
if (diff.acceptedOrCommitted != null)
size += CommandSerializers.ballot.serializedSize(diff.acceptedOrCommitted, userVersion);
if (diff.promised != null)
size += CommandSerializers.ballot.serializedSize(diff.promised, userVersion);
if (diff.route != null)
size += AccordKeyspace.LocalVersionedSerializers.route.serializedSize(diff.route);
if (diff.partialTxn != null)
CommandSerializers.partialTxn.serializedSize(diff.partialTxn, userVersion);
if (diff.partialDeps != null)
DepsSerializer.partialDeps.serializedSize(diff.partialDeps, userVersion);
if (diff.additionalKeysOrRanges != null)
KeySerializers.seekables.serializedSize(diff.additionalKeysOrRanges, userVersion);
if (diff.waitingOn != null)
{
size += Integer.BYTES;
size += WaitingOnSerializer.serializedSize(diff.txnId, diff.waitingOn);
}
if (diff.writes != null)
CommandSerializers.writes.serializedSize(diff.writes, userVersion);
if (diff.listeners != null && !diff.listeners.isEmpty())
{
size += Byte.BYTES;
for (Command.DurableAndIdempotentListener listener : diff.listeners)
size += AccordKeyspace.LocalVersionedSerializers.listeners.serializedSize(listener);
}
return (int) size;
}
@Override
public void serialize(JournalKey key, Object value, DataOutputPlus out, int userVersion) throws IOException
{
SavedDiff diff = (SavedDiff) value;
int flags = getFlags(diff);
out.writeShort(flags);
if (diff.txnId != null)
CommandSerializers.txnId.serialize(diff.txnId, out, userVersion);
if (diff.executeAt != null)
CommandSerializers.timestamp.serialize(diff.executeAt, out, userVersion);
if (diff.saveStatus != null)
out.writeInt(diff.saveStatus.ordinal());
if (diff.durability != null)
out.writeInt(diff.durability.ordinal());
if (diff.acceptedOrCommitted != null)
CommandSerializers.ballot.serialize(diff.acceptedOrCommitted, out, userVersion);
if (diff.promised != null)
CommandSerializers.ballot.serialize(diff.promised, out, userVersion);
if (diff.route != null)
AccordKeyspace.LocalVersionedSerializers.route.serialize(diff.route, out); // TODO (required): user version
if (diff.partialTxn != null)
CommandSerializers.partialTxn.serialize(diff.partialTxn, out, userVersion);
if (diff.partialDeps != null)
DepsSerializer.partialDeps.serialize(diff.partialDeps, out, userVersion);
if (diff.additionalKeysOrRanges != null)
KeySerializers.seekables.serialize(diff.additionalKeysOrRanges, out, userVersion);
if (diff.waitingOn != null)
{
long size = WaitingOnSerializer.serializedSize(diff.txnId, diff.waitingOn);
ByteBuffer serialized = WaitingOnSerializer.serialize(diff.txnId, diff.waitingOn);
out.writeInt((int) size);
out.write(serialized);
}
if (diff.writes != null)
CommandSerializers.writes.serialize(diff.writes, out, userVersion);
if (diff.listeners != null && !diff.listeners.isEmpty())
{
out.writeByte(diff.listeners.size());
for (Command.DurableAndIdempotentListener listener : diff.listeners)
AccordKeyspace.LocalVersionedSerializers.listeners.serialize(listener, out);
}
}
private static int getFlags(SavedDiff diff)
{
int flags = 0;
if (diff.txnId != null)
flags = setBit(flags, HasFields.TXN_ID.ordinal());
if (diff.executeAt != null)
flags = setBit(flags, HasFields.EXECUTE_AT.ordinal());
if (diff.saveStatus != null)
flags = setBit(flags, HasFields.SAVE_STATUS.ordinal());
if (diff.durability != null)
flags = setBit(flags, HasFields.DURABILITY.ordinal());
if (diff.acceptedOrCommitted != null)
flags = setBit(flags, HasFields.ACCEPTED.ordinal());
if (diff.promised != null)
flags = setBit(flags, HasFields.PROMISED.ordinal());
if (diff.route != null)
flags = setBit(flags, HasFields.ROUTE.ordinal());
if (diff.partialTxn != null)
flags = setBit(flags, HasFields.PARTIAL_TXN.ordinal());
if (diff.partialDeps != null)
flags = setBit(flags, HasFields.PARTIAL_DEPS.ordinal());
if (diff.additionalKeysOrRanges != null)
flags = setBit(flags, HasFields.ADDITIONAL_KEYS.ordinal());
if (diff.waitingOn != null)
flags = setBit(flags, HasFields.WAITING_ON.ordinal());
if (diff.writes != null)
flags = setBit(flags, HasFields.WRITES.ordinal());
if (diff.listeners != null && !diff.listeners.isEmpty())
flags = setBit(flags, HasFields.LISTENERS.ordinal());
return flags;
}
@Override
public Object deserialize(JournalKey key, DataInputPlus in, int userVersion) throws IOException
{
int flags = in.readShort();
TxnId txnId = null;
Timestamp executedAt = null;
SaveStatus saveStatus = null;
Status.Durability durability = null;
Ballot acceptedOrCommitted = null;
Ballot promised = null;
Route<?> route = null;
PartialTxn partialTxn = null;
PartialDeps partialDeps = null;
Seekables<?, ?> additionalKeysOrRanges = null;
WaitingOnProvider waitingOn = (txn, deps) -> null;
Writes writes = null;
Listeners.Immutable listeners = null;
if (isSet(flags, HasFields.TXN_ID.ordinal()))
txnId = CommandSerializers.txnId.deserialize(in, userVersion);
if (isSet(flags, HasFields.EXECUTE_AT.ordinal()))
executedAt = CommandSerializers.timestamp.deserialize(in, userVersion);
if (isSet(flags, HasFields.SAVE_STATUS.ordinal()))
saveStatus = SaveStatus.values()[in.readInt()];
if (isSet(flags, HasFields.DURABILITY.ordinal()))
durability = Status.Durability.values()[in.readInt()];
if (isSet(flags, HasFields.ACCEPTED.ordinal()))
acceptedOrCommitted = CommandSerializers.ballot.deserialize(in, userVersion);
if (isSet(flags, HasFields.PROMISED.ordinal()))
promised = CommandSerializers.ballot.deserialize(in, userVersion);
if (isSet(flags, HasFields.ROUTE.ordinal()))
route = AccordKeyspace.LocalVersionedSerializers.route.deserialize(in);
if (isSet(flags, HasFields.PARTIAL_TXN.ordinal()))
partialTxn = CommandSerializers.partialTxn.deserialize(in, userVersion);
if (isSet(flags, HasFields.PARTIAL_DEPS.ordinal()))
partialDeps = DepsSerializer.partialDeps.deserialize(in, userVersion);
if (isSet(flags, HasFields.ADDITIONAL_KEYS.ordinal()))
additionalKeysOrRanges = KeySerializers.seekables.deserialize(in, userVersion);
if (isSet(flags, HasFields.WAITING_ON.ordinal()))
{
int size = in.readInt();
byte[] bytes = new byte[size];
in.readFully(bytes);
ByteBuffer buffer = ByteBuffer.wrap(bytes);
waitingOn = (localTxnId, deps) -> {
try
{
return WaitingOnSerializer.deserialize(localTxnId, deps.keyDeps.keys(), deps.rangeDeps.txnIds(), buffer);
}
catch (IOException e)
{
throw Throwables.unchecked(e);
}
};
}
if (isSet(flags, HasFields.WRITES.ordinal()))
writes = CommandSerializers.writes.deserialize(in, userVersion);
if (isSet(flags, HasFields.LISTENERS.ordinal()))
{
Listeners builder = Listeners.Immutable.EMPTY.mutable();
int cnt = in.readByte();
for (int i = 0; i < cnt; i++)
builder.add(AccordKeyspace.LocalVersionedSerializers.listeners.deserialize(in));
listeners = new Listeners.Immutable(builder);
}
return new LoadedDiff(txnId,
executedAt,
saveStatus,
durability,
acceptedOrCommitted,
promised,
route,
partialTxn,
partialDeps,
additionalKeysOrRanges,
waitingOn,
writes,
listeners);
}
}
static int setBit(int value, int bit)
{
return value | (1 << bit);
}
static boolean isSet(int value, int bit)
{
return (value & (1 << bit)) != 0;
}
public interface WaitingOnProvider
{
Command.WaitingOn provide(TxnId txnId, PartialDeps deps);
}
}

View File

@ -17,7 +17,9 @@
*/
package org.apache.cassandra.service.accord.async;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.TreeMap;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@ -29,6 +31,7 @@ import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import accord.api.Key;
import accord.local.Command;
import accord.local.CommandStore;
import accord.local.PreLoadContext;
import accord.local.SafeCommandStore;
@ -36,6 +39,7 @@ import accord.primitives.Seekables;
import accord.primitives.TxnId;
import accord.utils.Invariants;
import accord.utils.async.AsyncChains;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.service.accord.AccordCommandStore;
import org.apache.cassandra.service.accord.AccordSafeCommand;
import org.apache.cassandra.service.accord.AccordSafeCommandStore;
@ -43,6 +47,7 @@ import org.apache.cassandra.service.accord.AccordSafeCommandsForKey;
import org.apache.cassandra.service.accord.AccordSafeCommandsForRanges;
import org.apache.cassandra.service.accord.AccordSafeState;
import org.apache.cassandra.service.accord.AccordSafeTimestampsForKey;
import org.apache.cassandra.service.accord.SavedCommand;
import static org.apache.cassandra.service.accord.async.AsyncLoader.txnIds;
import static org.apache.cassandra.service.accord.async.AsyncOperation.State.COMPLETING;
@ -90,7 +95,7 @@ public abstract class AsyncOperation<R> extends AsyncChains.Head<R> implements R
enum State
{
INITIALIZED, LOADING, PREPARING, RUNNING, COMPLETING, FINISHED, FAILED;
INITIALIZED, LOADING, PREPARING, RUNNING, COMPLETING, AWAITING_FLUSH, FINISHED, FAILED;
boolean isComplete()
{
@ -108,6 +113,8 @@ public abstract class AsyncOperation<R> extends AsyncChains.Head<R> implements R
private final String loggingId;
private BiConsumer<? super R, Throwable> callback;
private List<Command> sanityCheck = null;
private void setLoggingIds()
{
MDC.put(LoggingProps.COMMAND_STORE, commandStore.loggingId);
@ -159,11 +166,6 @@ public abstract class AsyncOperation<R> extends AsyncChains.Head<R> implements R
}
}
void onUnblocked()
{
commandStore.executor().execute(this);
}
private void state(State state)
{
this.state = state;
@ -182,12 +184,6 @@ public abstract class AsyncOperation<R> extends AsyncChains.Head<R> implements R
}
}
@Nullable
TxnId primaryTxnId()
{
return preLoadContext.primaryTxnId();
}
@SuppressWarnings("unchecked")
Seekables<?, ?> keys()
{
@ -215,7 +211,6 @@ public abstract class AsyncOperation<R> extends AsyncChains.Head<R> implements R
commandStore.abortCurrentOperation();
case LOADING:
context.releaseResources(commandStore);
commandStore.executionOrder().unregisterOutOfOrder(this);
case INITIALIZED:
break; // nothing to clean up, call callback
}
@ -233,30 +228,50 @@ public abstract class AsyncOperation<R> extends AsyncChains.Head<R> implements R
protected void runInternal()
{
Boolean canRun = null;
switch (state)
{
default: throw new IllegalStateException("Unexpected state " + state);
case INITIALIZED:
canRun = commandStore.executionOrder().register(this);
if (Invariants.isParanoid())
Invariants.checkState(canRun.booleanValue() == commandStore.executionOrder().canRun(this), "Register of %s returned canRun=%s but canRun returned %s!", this, canRun, !canRun);
state(LOADING);
case LOADING:
if (null == canRun)
canRun = commandStore.executionOrder().canRun(this);
if (!loader.load(context, this::onLoaded) || !canRun)
if (!loader.load(context, this::onLoaded))
return;
state(PREPARING);
case PREPARING:
safeStore = commandStore.beginOperation(preLoadContext, context.commands, context.timestampsForKey, context.commandsForKey, context.commandsForRanges);
state(RUNNING);
case RUNNING:
result = apply(safeStore);
// TODO (required): currently, we are not very efficient about ensuring that we persist the absolute minimum amount of state. Improve that.
List<SavedCommand.SavedDiff> diffs = null;
for (AccordSafeCommand commandState : context.commands.values())
{
SavedCommand.SavedDiff diff = commandState.diff();
if (diff != null)
{
if (diffs == null)
diffs = new ArrayList<>(context.commands.size());
diffs.add(diff);
if (CassandraRelevantProperties.DTEST_ACCORD_JOURNAL_SANITY_CHECK_ENABLED.getBoolean())
{
if (sanityCheck == null)
sanityCheck = new ArrayList<>(context.commands.size());
sanityCheck.add(commandState.current());
}
}
}
safeStore.postExecute(context.commands, context.timestampsForKey, context.commandsForKey, context.commandsForRanges);
context.releaseResources(commandStore);
commandStore.completeOperation(safeStore);
commandStore.executionOrder().unregister(this);
if (diffs != null)
{
state(COMPLETING);
this.commandStore.appendCommands(diffs, sanityCheck, () -> finish(result, null));
return;
}
state(COMPLETING);
case COMPLETING:
finish(result, null);
@ -326,6 +341,7 @@ public abstract class AsyncOperation<R> extends AsyncChains.Head<R> implements R
return new ForFunction<>((AccordCommandStore) commandStore, loadCtx, function);
}
// TODO (desired): these anonymous ops are somewhat tricky to debug. We may want to at least give them names.
static class ForConsumer extends AsyncOperation<Void>
{
private final Consumer<? super SafeCommandStore> consumer;

View File

@ -1,390 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cassandra.service.accord.async;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.function.Consumer;
import accord.api.Key;
import accord.api.RoutingKey;
import accord.primitives.Range;
import accord.primitives.Seekable;
import accord.primitives.TxnId;
import accord.utils.Invariants;
import org.agrona.collections.Object2ObjectHashMap;
import org.apache.cassandra.service.accord.RangeTreeRangeAccessor;
import org.apache.cassandra.utils.RTree;
import org.apache.cassandra.utils.RangeTree;
/**
* Assists with correct ordering of {@link AsyncOperation} execution wrt each other,
* preventing reordering of overlapping operations by {@link AsyncLoader}.
*/
public class ExecutionOrder
{
private static class Conflicts
{
private final List<Key> keyConflicts;
private final List<Range> rangeConflicts;
private Conflicts(List<Key> keyConflicts, List<Range> rangeConflicts)
{
this.keyConflicts = keyConflicts;
this.rangeConflicts = rangeConflicts;
}
}
private class RangeState
{
private final Range range;
private final IdentityHashMap<AsyncOperation<?>, Conflicts> operationToConflicts = new IdentityHashMap<>();
private Object operationOrQueue;
public RangeState(Range range, List<Key> keyConflicts, List<Range> rangeConflicts, AsyncOperation<?> operation)
{
this.range = range;
this.operationOrQueue = operation;
add(operation, keyConflicts, rangeConflicts);
}
public void add(AsyncOperation<?> operation, List<Key> keyConflicts, List<Range> rangeConflicts)
{
operationToConflicts.put(operation, new Conflicts(keyConflicts, rangeConflicts));
}
boolean canRun(AsyncOperation<?> operation)
{
if (operationOrQueue instanceof AsyncOperation<?>)
{
Invariants.checkState(operationOrQueue == operation);
return true;
}
else
{
ArrayDeque<AsyncOperation<?>> queue = (ArrayDeque<AsyncOperation<?>>) operationOrQueue;
return queue.peek() == operation;
}
}
Conflicts remove(AsyncOperation<?> operation, boolean allowOutOfOrder)
{
unregister("range", range, operationOrQueue, operation, allowOutOfOrder, () -> rangeQueues.remove(range));
return operationToConflicts.remove(operation);
}
public Conflicts conflicts(AsyncOperation<?> operation)
{
return operationToConflicts.get(operation);
}
}
private final Object2ObjectHashMap<Object, Object> queues = new Object2ObjectHashMap<>();
private final RangeTree<RoutingKey, Range, RangeState> rangeQueues = RTree.create(RangeTreeRangeAccessor.instance);
/**
* Register an operation as having a dependency on its keys and TxnIds
* @return true if no other operation depends on the keys or TxnIds, false otherwise
*/
boolean register(AsyncOperation<?> operation)
{
boolean canRun = true;
for (Seekable seekable : operation.keys())
{
switch (seekable.domain())
{
case Key:
canRun &= register(seekable.asKey(), operation);
break;
case Range:
canRun &= register(seekable.asRange(), operation);
break;
default:
throw new AssertionError("Unexpected domain: " + seekable.domain());
}
}
TxnId primaryTxnId = operation.primaryTxnId();
if (null != primaryTxnId)
canRun &= register(primaryTxnId, operation);
return canRun;
}
private boolean register(Range range, AsyncOperation<?> operation)
{
// Ranges depend on Ranges and Keys
// Keys depend on Keys...
// This adds a complication to this logic as keys should be able to make progress regardless of ranges, but rangest must depend on keys
List<Key> keyConflicts = null;
for (Object o : queues.keySet())
{
if (!(o instanceof Key))
continue;
Key key = (Key) o;
if (!range.contains(key))
continue;
if (keyConflicts == null)
keyConflicts = new ArrayList<>();
keyConflicts.add(key);
}
if (keyConflicts != null)
keyConflicts.forEach(k -> register(k, operation));
class Result
{
RangeState sameRange = null;
List<Range> rangeConflicts = null;
}
Result result = new Result();
rangeQueues.search(range, e -> {
if (range.equals(e.getKey()))
result.sameRange = e.getValue();
else
{
if (result.rangeConflicts == null)
result.rangeConflicts = new ArrayList<>();
result.rangeConflicts.add(e.getKey());
}
RangeState state = e.getValue();
// a single range could conflict with multiple other ranges, so it is possible that the operation
// exists in the queue already due to another range in the txn... simple example is
// keys = (0, 10], (12, 15]
// e.getKey() == (-100, 100]
// in this case the operation would attempt to double add since it has 2 keys that conflict with this single range
register(state.operationOrQueue, operation, q -> state.operationOrQueue = q);
});
if (result.sameRange != null)
{
result.sameRange.add(operation, keyConflicts, result.rangeConflicts);
}
else
{
rangeQueues.add(range, new RangeState(range, keyConflicts, result.rangeConflicts, operation));
}
return keyConflicts == null && result.rangeConflicts == null && result.sameRange == null;
}
/**
* Register an operation as having a dependency on a key or a TxnId
* @return true if no other operation depends on the key/TxnId, false otherwise
*/
private boolean register(Object keyOrTxnId, AsyncOperation<?> operation)
{
Object operationOrQueue = queues.get(keyOrTxnId);
if (null == operationOrQueue)
{
queues.put(keyOrTxnId, operation);
return true;
}
register(operationOrQueue, operation, q -> queues.put(keyOrTxnId, q));
return false;
}
private void register(Object operationOrQueue, AsyncOperation<?> operation, Consumer<ArrayDeque<AsyncOperation<?>>> onCreateQueue)
{
if (operationOrQueue instanceof AsyncOperation)
{
Invariants.checkState(operationOrQueue != operation, "Attempted to double register operation %s", operation);
ArrayDeque<AsyncOperation<?>> queue = new ArrayDeque<>(4);
queue.add((AsyncOperation<?>) operationOrQueue);
queue.add(operation);
onCreateQueue.accept(queue);
}
else
{
@SuppressWarnings("unchecked")
ArrayDeque<AsyncOperation<?>> queue = (ArrayDeque<AsyncOperation<?>>) operationOrQueue;
queue.add(operation);
}
}
/**
* Unregister the operation as being a dependency for its keys and TxnIds, but do so even if it is unable to run now.
*/
void unregisterOutOfOrder(AsyncOperation<?> operation)
{
unregister(operation, true);
}
/**
* Unregister the operation as being a dependency for its keys and TxnIds
*/
void unregister(AsyncOperation<?> operation)
{
unregister(operation, false);
}
private void unregister(AsyncOperation<?> operation, boolean allowOutOfOrder)
{
for (Seekable seekable : operation.keys())
{
switch (seekable.domain())
{
case Key:
unregister(seekable.asKey(), operation, allowOutOfOrder);
break;
case Range:
unregister(seekable.asRange(), operation, allowOutOfOrder);
break;
default:
throw new AssertionError("Unexpected domain: " + seekable.domain());
}
}
TxnId primaryTxnId = operation.primaryTxnId();
if (null != primaryTxnId)
unregister(primaryTxnId, operation, allowOutOfOrder);
}
private void unregister(Range range, AsyncOperation<?> operation, boolean allowOutOfOrder)
{
RangeState state = state(range);
Conflicts conflicts = state.remove(operation, allowOutOfOrder);
if (conflicts.rangeConflicts != null)
conflicts.rangeConflicts.forEach(r -> state(r).remove(operation, allowOutOfOrder));
if (conflicts.keyConflicts != null)
conflicts.keyConflicts.forEach(k -> unregister(k, operation, allowOutOfOrder));
}
/**
* Unregister the operation as being a dependency for key or TxnId
*/
private void unregister(Object keyOrTxnId, AsyncOperation<?> operation, boolean allowOutOfOrder)
{
Object operationOrQueue = queues.get(keyOrTxnId);
Invariants.nonNull(operationOrQueue);
unregister("Key or TxnId", keyOrTxnId, operationOrQueue, operation, allowOutOfOrder, () -> queues.remove(keyOrTxnId));
}
private void unregister(String name, Object key, Object operationOrQueue, AsyncOperation<?> operation, boolean allowOutOfOrder, Runnable onEmpty)
{
if (operationOrQueue instanceof AsyncOperation<?>)
{
Invariants.checkState(operationOrQueue == operation, "Only single operation present and was not %s; %s %s", name, key);
onEmpty.run();
}
else
{
@SuppressWarnings("unchecked")
ArrayDeque<AsyncOperation<?>> queue = (ArrayDeque<AsyncOperation<?>>) operationOrQueue;
if (allowOutOfOrder)
{
Invariants.checkState(queue.remove(operation), "Operation %s was not found in queue: %s; %s %s", operation, queue, name, key);
}
else
{
Invariants.checkState(queue.peek() == operation, "Operation %s is not at the top of the queue; %s; %s %s", operation, queue, name, key);
queue.poll();
}
if (queue.isEmpty())
{
onEmpty.run();
}
else
{
AsyncOperation<?> next = queue.peek();
if (next == operation)
{
// a single range could conflict with multiple other ranges, so it is possible that the operation
// exists in the queue already due to another range in the txn... simple example is
// keys = (0, 10], (12, 15]
// e.getKey() == (-100, 100]
// in this case the operation would attempt to double add since it has 2 keys that conflict with this single range
return;
}
if (canRun(next))
next.onUnblocked();
}
}
}
boolean canRun(AsyncOperation<?> operation)
{
for (Seekable seekable : operation.keys())
{
switch (seekable.domain())
{
case Key:
if (!canRun(seekable.asKey(), operation))
return false;
break;
case Range:
if (!canRun(seekable.asRange(), operation))
return false;
break;
default:
throw new AssertionError("Unexpected domain: " + seekable.domain());
}
}
TxnId primaryTxnId = operation.primaryTxnId();
return primaryTxnId == null || canRun(primaryTxnId, operation);
}
private boolean canRun(Range range, AsyncOperation<?> operation)
{
RangeState state = state(range);
if (!state.canRun(operation))
return false;
Conflicts conflicts = state.conflicts(operation);
if (conflicts.rangeConflicts != null)
{
for (Range r : conflicts.rangeConflicts)
{
if (!state(r).canRun(operation))
return false;
}
}
if (conflicts.keyConflicts != null)
{
for (Key key : conflicts.keyConflicts)
{
if (!canRun(key, operation))
return false;
}
}
return true;
}
private RangeState state(Range range)
{
List<RangeState> list = rangeQueues.get(range);
assert list.size() == 1 : String.format("Expected 1 element for range %s but saw list %s", range, list);
return list.get(0);
}
private boolean canRun(Object keyOrTxnId, AsyncOperation<?> operation)
{
Object operationOrQueue = queues.get(keyOrTxnId);
Invariants.nonNull(operationOrQueue);
if (operationOrQueue instanceof AsyncOperation<?>)
{
Invariants.checkState(operationOrQueue == operation);
return true;
}
@SuppressWarnings("unchecked")
ArrayDeque<AsyncOperation<?>> queue = (ArrayDeque<AsyncOperation<?>>) operationOrQueue;
return queue.peek() == operation;
}
}

View File

@ -0,0 +1,89 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cassandra.distributed.test.accord;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.ConsistencyLevel;
import org.apache.cassandra.distributed.shared.WithProperties;
import org.apache.cassandra.distributed.test.TestBaseImpl;
import org.apache.cassandra.utils.concurrent.CountDownLatch;
public class AccordJournalTest extends TestBaseImpl
{
@Test
public void saveLoadSanityCheck() throws Throwable
{
String timeout = "10s";
try (WithProperties wp = new WithProperties().set(CassandraRelevantProperties.DTEST_ACCORD_JOURNAL_SANITY_CHECK_ENABLED, "true");
Cluster cluster = init(Cluster.build(1)
.withoutVNodes()
.withConfig(c -> c
.set("read_request_timeout", timeout)
.set("transaction_timeout", timeout)
)
.start()))
{
final String TABLE = KEYSPACE + ".test_table";
cluster.schemaChange("CREATE TABLE " + TABLE + " (k int, c int, v int, primary key (k, c)) WITH transactional_mode='full'");
List<Thread> threads = new ArrayList<>();
int numThreads = 10;
CountDownLatch latch = CountDownLatch.newCountDownLatch(numThreads);
AtomicInteger counter = new AtomicInteger();
for (int i = 0; i < numThreads; i++)
{
int finalI = i;
Thread t = new Thread(() -> {
latch.decrement();
latch.awaitUninterruptibly();
try
{
for (int j = 0; j < 100; j++)
{
cluster.coordinator(1).execute("BEGIN TRANSACTION\n" +
"INSERT INTO " + TABLE + "(k, c, v) VALUES (?, ?, ?);\n" +
"INSERT INTO " + TABLE + "(k, c, v) VALUES (?, ?, ?);\n" +
"COMMIT TRANSACTION",
ConsistencyLevel.ALL,
1, j, finalI * 100 + j,
2, j, finalI * 100 + j);
counter.incrementAndGet();
}
}
catch (Throwable throwable)
{
throwable.printStackTrace();
}
});
t.start();
threads.add(t);
}
for (Thread thread : threads)
thread.join();
cluster.coordinator(1).execute("SELECT * FROM " + TABLE + " WHERE k = ?;", ConsistencyLevel.SERIAL, 1);
}
}
}

View File

@ -35,9 +35,9 @@ import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.filesystem.ListenableFileSystem;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.journal.AsyncCallbacks;
import org.apache.cassandra.journal.Journal;
import org.apache.cassandra.journal.KeySupport;
import org.apache.cassandra.journal.RecordPointer;
import org.apache.cassandra.journal.ValueSerializer;
import org.junit.Assert;
@ -60,28 +60,27 @@ public class AccordJournalSimulationTest extends SimulationTestBase
public void simpleRWTest()
{
simulate(arr(() -> {
ListenableFileSystem fs = new ListenableFileSystem(Jimfs.newFileSystem());
File.unsafeSetFilesystem(fs);
DatabaseDescriptor.daemonInitialization();
DatabaseDescriptor.setCommitLogCompression(new ParameterizedClass("LZ4Compressor", ImmutableMap.of())); //
DatabaseDescriptor.setCommitLogWriteDiskAccessMode(Config.DiskAccessMode.standard);
DatabaseDescriptor.initializeCommitLogDiskAccessMode();
DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
DatabaseDescriptor.setAccordJournalDirectory("/journal");
new File("/journal").createDirectoriesIfNotExists();
ListenableFileSystem fs = new ListenableFileSystem(Jimfs.newFileSystem());
File.unsafeSetFilesystem(fs);
DatabaseDescriptor.daemonInitialization();
DatabaseDescriptor.setCommitLogCompression(new ParameterizedClass("LZ4Compressor", ImmutableMap.of())); //
DatabaseDescriptor.setCommitLogWriteDiskAccessMode(Config.DiskAccessMode.standard);
DatabaseDescriptor.initializeCommitLogDiskAccessMode();
DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
DatabaseDescriptor.setAccordJournalDirectory("/journal");
new File("/journal").createDirectoriesIfNotExists();
DatabaseDescriptor.setDumpHeapOnUncaughtException(false);
DatabaseDescriptor.setDumpHeapOnUncaughtException(false);
Keyspace.setInitialized();
Keyspace.setInitialized();
State.journal = new Journal<>("AccordJournal",
new File("/journal"),
new AccordSpec.JournalSpec(),
new TestCallbacks(),
new IdentityKeySerializer(),
new IdentityValueSerializer());
}),
() -> check());
State.journal = new Journal<>("AccordJournal",
new File("/journal"),
new AccordSpec.JournalSpec(),
new IdentityKeySerializer(),
new IdentityValueSerializer());
}),
() -> check());
}
public static void check()
@ -93,7 +92,10 @@ public class AccordJournalSimulationTest extends SimulationTestBase
for (int i = 0; i < count; i++)
{
int finalI = i;
State.executor.submit(() -> State.journal.asyncWrite("test" + finalI, "test" + finalI, Collections.singleton(1), null));
State.executor.submit(() -> {
RecordPointer ptr = State.journal.asyncWrite("test" + finalI, "test" + finalI, Collections.singleton(1));
State.journal.onFlush(ptr, State.latch::decrement);
});
}
State.latch.await();
@ -123,34 +125,6 @@ public class AccordJournalSimulationTest extends SimulationTestBase
}
}
public static class TestCallbacks implements AsyncCallbacks<String, String>
{
@Override
public void onWrite(long segment, int position, int size, String key, String value, Object writeContext)
{
State.latch.decrement();
}
@Override
public void onWriteFailed(String key, String value, Object writeContext, Throwable cause)
{
State.thrown.add(new IllegalStateException("Write failed for " + key));
State.latch.decrement();
}
@Override
public void onFlush(long segment, int position)
{
}
@Override
public void onFlushFailed(Throwable cause)
{
State.thrown.add(new RuntimeException("Could not flush", cause));
}
}
@Isolated
public static class IdentityValueSerializer implements ValueSerializer<String, String>
{

View File

@ -122,9 +122,9 @@ import static org.apache.cassandra.simulator.cluster.ClusterActions.Options.noAc
*
* And then run your test using the following settings (omit add-* if you are running on jdk8):
*
-Dstorage-config=$MODULE_DIR$/test/conf
-Dstorage-config=/Users/ifesdjeen/p/java/cassandra-accord/test/conf
-Djava.awt.headless=true
-javaagent:$MODULE_DIR$/lib/jamm-0.4.0.jar
-javaagent:/Users/ifesdjeen/p/java/cassandra-accord/lib/jamm-0.4.0.jar
-ea
-Dcassandra.debugrefcount=true
-Xss384k
@ -146,15 +146,15 @@ import static org.apache.cassandra.simulator.cluster.ClusterActions.Options.noAc
-Dcassandra.test.messagingService.nonGracefulShutdown=true
-Dcassandra.use_nix_recursive_delete=true
-Dcie-cassandra.disable_schema_drop_log=true
-Dlogback.configurationFile=file://$MODULE_DIR$/test/conf/logback-simulator.xml
-Dlogback.configurationFile=file:///Users/ifesdjeen/p/java/cassandra-accord/test/conf/logback-simulator.xml
-Dcassandra.ring_delay_ms=10000
-Dcassandra.tolerate_sstable_size=true
-Dcassandra.skip_sync=true
-Dcassandra.debugrefcount=false
-Dcassandra.test.simulator.determinismcheck=strict
-Dcassandra.test.simulator.print_asm=none
-javaagent:$MODULE_DIR$/build/test/lib/jars/simulator-asm.jar
-Xbootclasspath/a:$MODULE_DIR$/build/test/lib/jars/simulator-bootstrap.jar
-javaagent:/Users/ifesdjeen/p/java/cassandra-accord/build/test/lib/jars/simulator-asm.jar
-Xbootclasspath/a:/Users/ifesdjeen/p/java/cassandra-accord/build/test/lib/jars/simulator-bootstrap.jar
-XX:ActiveProcessorCount=4
-XX:-TieredCompilation
-XX:-BackgroundCompilation

View File

@ -246,7 +246,7 @@ public final class ServerTestUtils
}
}
private static void cleanupDirectory(String dirName)
public static void cleanupDirectory(String dirName)
{
if (dirName != null)
cleanupDirectory(new File(dirName));

View File

@ -42,6 +42,7 @@ import static org.apache.cassandra.cql3.statements.TransactionStatement.NO_CONDI
import static org.apache.cassandra.cql3.statements.TransactionStatement.NO_COUNTERS_IN_TXNS_MESSAGE;
import static org.apache.cassandra.cql3.statements.TransactionStatement.NO_TIMESTAMPS_IN_UPDATES_MESSAGE;
import static org.apache.cassandra.cql3.statements.TransactionStatement.SELECT_REFS_NEED_COLUMN_MESSAGE;
import static org.apache.cassandra.cql3.statements.TransactionStatement.TRANSACTIONS_DISABLED_ON_TABLE_MESSAGE;
import static org.apache.cassandra.cql3.statements.UpdateStatement.CANNOT_SET_KEY_WITH_REFERENCE_MESSAGE;
import static org.apache.cassandra.cql3.statements.UpdateStatement.UPDATING_PRIMARY_KEY_MESSAGE;
import static org.apache.cassandra.cql3.statements.schema.CreateTableStatement.parse;
@ -57,18 +58,20 @@ public class TransactionStatementTest
private static final TableId TABLE4_ID = TableId.fromString("00000000-0000-0000-0000-000000000004");
private static final TableId TABLE5_ID = TableId.fromString("00000000-0000-0000-0000-000000000005");
private static final TableId TABLE6_ID = TableId.fromString("00000000-0000-0000-0000-000000000006");
private static final TableId TABLE7_ID = TableId.fromString("00000000-0000-0000-0000-000000000007");
@BeforeClass
public static void beforeClass() throws Exception
{
SchemaLoader.prepareServer();
SchemaLoader.createKeyspace("ks", KeyspaceParams.simple(1),
parse("CREATE TABLE tbl1 (k int, c int, v int, primary key (k, c))", "ks").id(TABLE1_ID),
parse("CREATE TABLE tbl2 (k int, c int, v int, primary key (k, c))", "ks").id(TABLE2_ID),
parse("CREATE TABLE tbl3 (k int PRIMARY KEY, \"with spaces\" int, \"with\"\"quote\" int, \"MiXeD_CaSe\" int)", "ks").id(TABLE3_ID),
parse("CREATE TABLE tbl4 (k int PRIMARY KEY, int_list list<int>)", "ks").id(TABLE4_ID),
parse("CREATE TABLE tbl5 (k int PRIMARY KEY, v int)", "ks").id(TABLE5_ID),
parse("CREATE TABLE tbl6 (k int PRIMARY KEY, c counter)", "ks").id(TABLE6_ID));
parse("CREATE TABLE tbl1 (k int, c int, v int, PRIMARY KEY (k, c)) WITH transactional_mode = 'full'", "ks").id(TABLE1_ID),
parse("CREATE TABLE tbl2 (k int, c int, v int, primary key (k, c)) WITH transactional_mode = 'full'", "ks").id(TABLE2_ID),
parse("CREATE TABLE tbl3 (k int PRIMARY KEY, \"with spaces\" int, \"with\"\"quote\" int, \"MiXeD_CaSe\" int) WITH transactional_mode = 'full'", "ks").id(TABLE3_ID),
parse("CREATE TABLE tbl4 (k int PRIMARY KEY, int_list list<int>) WITH transactional_mode = 'full'", "ks").id(TABLE4_ID),
parse("CREATE TABLE tbl5 (k int PRIMARY KEY, v int) WITH transactional_mode = 'full'", "ks").id(TABLE5_ID),
parse("CREATE TABLE tbl6 (k int PRIMARY KEY, c counter) WITH transactional_mode = 'full'", "ks").id(TABLE6_ID),
parse("CREATE TABLE tbl7 (k int PRIMARY KEY, v int) WITH transactional_mode = 'off'", "ks").id(TABLE7_ID));
}
@Test
@ -400,6 +403,43 @@ public class TransactionStatementTest
.hasMessageContaining(String.format(ILLEGAL_RANGE_QUERY_MESSAGE, "LET assignment row1", "at [2:15]"));
}
@Test
public void shouldRejectLetSelectOnNonTransactionalTable()
{
String query = "BEGIN TRANSACTION\n" +
" LET row1 = (SELECT * FROM ks.tbl7 WHERE k = 0);\n" +
" INSERT INTO ks.tbl5 (k, v) VALUES (1, 2);\n" +
"COMMIT TRANSACTION;";
Assertions.assertThatThrownBy(() -> prepare(query))
.isInstanceOf(InvalidRequestException.class)
.hasMessageContaining(String.format(TRANSACTIONS_DISABLED_ON_TABLE_MESSAGE, "SELECT", "at [2:15]"));
}
@Test
public void shouldRejectSelectOnNonTransactionalTable()
{
String query = "BEGIN TRANSACTION\n" +
" SELECT * FROM ks.tbl7 WHERE k = 0;\n" +
"COMMIT TRANSACTION;";
Assertions.assertThatThrownBy(() -> prepare(query))
.isInstanceOf(InvalidRequestException.class)
.hasMessageContaining(String.format(TRANSACTIONS_DISABLED_ON_TABLE_MESSAGE, "SELECT", "at [2:3]"));
}
@Test
public void shouldRejectUpdateOnNonTransactionalTable()
{
String query = "BEGIN TRANSACTION\n" +
" INSERT INTO ks.tbl7 (k, v) VALUES (1, 2);\n" +
"COMMIT TRANSACTION;";
Assertions.assertThatThrownBy(() -> prepare(query))
.isInstanceOf(InvalidRequestException.class)
.hasMessageContaining(String.format(TRANSACTIONS_DISABLED_ON_TABLE_MESSAGE, "INSERT", "at [2:3]"));
}
private static CQLStatement prepare(String query)
{
TransactionStatement.Parsed parsed = (TransactionStatement.Parsed) QueryProcessor.parseStatement(query);

View File

@ -25,11 +25,15 @@ import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.distributed.shared.WithProperties;
import org.apache.cassandra.service.accord.*;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
@ -47,10 +51,6 @@ import accord.local.RedundantBefore;
import accord.local.SaveStatus;
import accord.local.Status;
import accord.local.Status.Durability;
import accord.messages.Accept;
import accord.messages.Apply;
import accord.messages.Commit;
import accord.messages.PreAccept;
import accord.primitives.Ballot;
import accord.primitives.Deps;
import accord.primitives.FullRoute;
@ -83,10 +83,6 @@ import org.apache.cassandra.schema.KeyspaceParams;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.service.accord.AccordCommandStore;
import org.apache.cassandra.service.accord.AccordKeyspace;
import org.apache.cassandra.service.accord.AccordTestUtils;
import org.apache.cassandra.service.accord.IAccordService;
import org.apache.cassandra.service.accord.api.PartitionKey;
import org.apache.cassandra.service.accord.serializers.CommandsForKeySerializer;
import org.apache.cassandra.utils.FBUtilities;
@ -99,9 +95,7 @@ import static accord.local.PreLoadContext.contextFor;
import static accord.utils.async.AsyncChains.getUninterruptibly;
import static org.apache.cassandra.Util.spinAssertEquals;
import static org.apache.cassandra.cql3.statements.schema.CreateTableStatement.parse;
import static org.apache.cassandra.db.compaction.CompactionAccordIteratorsTest.DurableBeforeType.MAJORITY;
import static org.apache.cassandra.db.compaction.CompactionAccordIteratorsTest.DurableBeforeType.NOT_DURABLE;
import static org.apache.cassandra.db.compaction.CompactionAccordIteratorsTest.DurableBeforeType.UNIVERSAL;
import static org.apache.cassandra.db.compaction.CompactionAccordIteratorsTest.DurableBeforeType.*;
import static org.apache.cassandra.schema.SchemaConstants.ACCORD_KEYSPACE_NAME;
import static org.apache.cassandra.service.accord.AccordKeyspace.*;
import static org.junit.Assert.*;
@ -113,7 +107,6 @@ import static org.mockito.Mockito.when;
public class CompactionAccordIteratorsTest
{
private static final Logger logger = LoggerFactory.getLogger(CompactionAccordIteratorsTest.class);
private static final long CLOCK_START = 44;
private static final long HLC_START = 41;
private static final int NODE = 1;
@ -124,7 +117,7 @@ public class CompactionAccordIteratorsTest
private static final TxnId SECOND_TXN_ID = AccordTestUtils.txnId(EPOCH, TXN_ID.hlc() + 1, NODE, Kind.Read);
private static final TxnId GT_TXN_ID = SECOND_TXN_ID;
// For CommandsForKey where we test with two commands
private static final TxnId[] TXN_IDS = new TxnId[] {TXN_ID, SECOND_TXN_ID};
private static final TxnId[] TXN_IDS = new TxnId[]{ TXN_ID, SECOND_TXN_ID };
private static final TxnId GT_SECOND_TXN_ID = AccordTestUtils.txnId(EPOCH, SECOND_TXN_ID.hlc() + 1, NODE);
static ColumnFamilyStore commands;
@ -146,7 +139,7 @@ public class CompactionAccordIteratorsTest
SchemaLoader.prepareServer();
// Schema doesn't matter since this is a metadata only test
SchemaLoader.createKeyspace("ks", KeyspaceParams.simple(1),
parse("CREATE TABLE tbl (k int, c int, v int, primary key (k, c))", "ks"));
parse("CREATE TABLE tbl (k int, c int, v int, PRIMARY KEY (k, c)) WITH transactional_mode = 'full'", "ks"));
StorageService.instance.initServer();
commands = ColumnFamilyStore.getIfExists(SchemaConstants.ACCORD_KEYSPACE_NAME, AccordKeyspace.COMMANDS);
@ -266,9 +259,9 @@ public class CompactionAccordIteratorsTest
assertEquals(1, partitions.size());
Partition partition = partitions.get(0);
PartitionKey partitionKey = new PartitionKey(partition.metadata().id, partition.partitionKey());
CommandsForKey cfk = CommandsForKeysAccessor.getCommandsForKey(partitionKey, ((Row)partition.unfilteredIterator().next()));
CommandsForKey cfk = CommandsForKeysAccessor.getCommandsForKey(partitionKey, ((Row) partition.unfilteredIterator().next()));
assertEquals(TXN_IDS.length, cfk.size());
for (int i = 0 ; i < TXN_IDS.length ; ++i)
for (int i = 0; i < TXN_IDS.length; ++i)
assertEquals(TXN_IDS[i], cfk.txnId(i));
};
}
@ -350,7 +343,7 @@ public class CompactionAccordIteratorsTest
Partition partition = partitions.get(0);
assertEquals(1, Iterators.size(partition.unfilteredIterator()));
ByteBuffer[] partitionKeyComponents = CommandRows.splitPartitionKey(partition.partitionKey());
Row row = (Row)partition.unfilteredIterator().next();
Row row = (Row) partition.unfilteredIterator().next();
assertEquals(CommandsColumns.TRUNCATE_FIELDS.length, row.columnCount());
for (ColumnMetadata cm : CommandsColumns.TRUNCATE_FIELDS)
assertNotNull(row.getColumnData(cm));
@ -369,7 +362,7 @@ public class CompactionAccordIteratorsTest
Partition partition = partitions.get(0);
assertEquals(1, Iterators.size(partition.unfilteredIterator()));
ByteBuffer[] partitionKeyComponents = CommandRows.splitPartitionKey(partition.partitionKey());
Row row = (Row)partition.unfilteredIterator().next();
Row row = (Row) partition.unfilteredIterator().next();
// execute_atleast is null, so when we read from the scanner the column won't be present in the partition
Assertions.assertThat(new ArrayList<>(row.columns())).isEqualTo(commands.metadata().regularColumns().stream().filter(c -> !c.name.toString().equals("execute_atleast")).collect(Collectors.toList()));
@ -447,11 +440,20 @@ public class CompactionAccordIteratorsTest
}
private void testWithCommandStore(TestWithCommandStore test, boolean additionalCommand) throws Throwable
{
try (WithProperties wp = new WithProperties().set(CassandraRelevantProperties.DTEST_ACCORD_JOURNAL_SANITY_CHECK_ENABLED, "true"))
{
testWithCommandStoreInternal(test, additionalCommand);
}
}
private void testWithCommandStoreInternal(TestWithCommandStore test, boolean additionalCommand) throws Throwable
{
Keyspace.open(ACCORD_KEYSPACE_NAME).getColumnFamilyStores().forEach(ColumnFamilyStore::truncateBlocking);
((AccordService) AccordService.instance()).journal().truncateForTesting();
clock.set(CLOCK_START);
AccordCommandStore commandStore = AccordTestUtils.createAccordCommandStore(clock::incrementAndGet, "ks", "tbl");
TxnId[] txnIds = additionalCommand ? TXN_IDS : new TxnId[] {TXN_ID};
TxnId[] txnIds = additionalCommand ? TXN_IDS : new TxnId[]{ TXN_ID };
Txn writeTxn = AccordTestUtils.createWriteTxn(42);
Txn readTxn = AccordTestUtils.createTxn(42);
Seekable key = writeTxn.keys().get(0);
@ -462,32 +464,20 @@ public class CompactionAccordIteratorsTest
PartialTxn partialTxn = txn.slice(commandStore.unsafeRangesForEpoch().currentRanges(), true);
PartialRoute<?> partialRoute = route.slice(commandStore.unsafeRangesForEpoch().currentRanges());
getUninterruptibly(commandStore.execute(contextFor(txnId, txn.keys(), COMMANDS), safe -> {
PreAccept preAccept =
PreAccept.SerializerSupport.create(txnId, partialRoute, txnId.epoch(), txnId.epoch(), false, txnId.epoch(), partialTxn, route);
commandStore.appendToJournal(preAccept);
CheckedCommands.preaccept(safe, txnId, partialTxn, route, null);
CheckedCommands.preaccept(safe, txnId, partialTxn, route, null, appendDiffToKeyspace(commandStore));
}).beginAsResult());
flush(commandStore);
getUninterruptibly(commandStore.execute(contextFor(txnId, txn.keys(), COMMANDS), safe -> {
Accept accept =
Accept.SerializerSupport.create(txnId, partialRoute, txnId.epoch(), txnId.epoch(), false, Ballot.ZERO, txnId, partialTxn.keys(), partialDeps);
commandStore.appendToJournal(accept);
CheckedCommands.accept(safe, txnId, Ballot.ZERO, partialRoute, partialTxn.keys(), null, txnId, partialDeps);
CheckedCommands.accept(safe, txnId, Ballot.ZERO, partialRoute, partialTxn.keys(), null, txnId, partialDeps, appendDiffToKeyspace(commandStore));
}).beginAsResult());
flush(commandStore);
getUninterruptibly(commandStore.execute(contextFor(txnId, txn.keys(), COMMANDS), safe -> {
Commit commit =
Commit.SerializerSupport.create(txnId, partialRoute, txnId.epoch(), Commit.Kind.StableFastPath, Ballot.ZERO, txnId, partialTxn.keys(), partialTxn, partialDeps, route, null);
commandStore.appendToJournal(commit);
CheckedCommands.commit(safe, SaveStatus.Stable, Ballot.ZERO, txnId, route, null, partialTxn, txnId, partialDeps);
CheckedCommands.commit(safe, SaveStatus.Stable, Ballot.ZERO, txnId, route, null, partialTxn, txnId, partialDeps, appendDiffToKeyspace(commandStore));
}).beginAsResult());
flush(commandStore);
getUninterruptibly(commandStore.execute(contextFor(txnId, txn.keys(), COMMANDS), safe -> {
Pair<Writes, Result> result = AccordTestUtils.processTxnResultDirect(safe, txnId, partialTxn, txnId);
Apply apply =
Apply.SerializationSupport.create(txnId, partialRoute, txnId.epoch(), Apply.Kind.Minimal, partialTxn.keys(), txnId, partialDeps, partialTxn, null, result.left, result.right);
commandStore.appendToJournal(apply);
CheckedCommands.apply(safe, txnId, route, null, txnId, partialDeps, partialTxn, result.left, result.right);
CheckedCommands.apply(safe, txnId, route, null, txnId, partialDeps, partialTxn, result.left, result.right, appendDiffToKeyspace(commandStore));
}).beginAsResult());
getUninterruptibly(commandStore.execute(contextFor(txnId, txn.keys(), COMMANDS), safe -> {
safe.get(txnId, txnId, route).addListener(new Command.ProxyListener(txnId)); // add a junk listener just to test it in compaction
@ -495,12 +485,15 @@ public class CompactionAccordIteratorsTest
flush(commandStore);
// The apply chain is asychronous, so it is easiest to just spin until it is applied
// in order to have the updated state in the system table
spinAssertEquals(true, 5, () ->
getUninterruptibly(commandStore.submit(contextFor(txnId, txn.keys(), COMMANDS), safe -> safe.get(txnId, route.homeKey()).current().hasBeen(Status.Applied)
).beginAsResult()));
spinAssertEquals(true, 5, () -> {
return getUninterruptibly(commandStore.submit(contextFor(txnId, txn.keys(), COMMANDS), safe -> {
Command command = safe.get(txnId, route.homeKey()).current();
appendDiffToKeyspace(commandStore).accept(null, command);
return command.hasBeen(Status.Applied);
}).beginAsResult());
});
flush(commandStore);
}
UntypedResultSet commandsTable = QueryProcessor.executeInternal("SELECT * FROM " + ACCORD_KEYSPACE_NAME + "." + AccordKeyspace.COMMANDS + ";");
logger.info(commandsTable.toStringUnsafe());
assertEquals(txnIds.length, commandsTable.size());
@ -510,13 +503,21 @@ public class CompactionAccordIteratorsTest
UntypedResultSet commandsForKeyTable = QueryProcessor.executeInternal("SELECT * FROM " + ACCORD_KEYSPACE_NAME + "." + COMMANDS_FOR_KEY + ";");
logger.info(commandsForKeyTable.toStringUnsafe());
assertEquals(1, commandsForKeyTable.size());
CommandsForKey cfk = CommandsForKeySerializer.fromBytes((Key)key, commandsForKeyTable.iterator().next().getBytes("data"));
CommandsForKey cfk = CommandsForKeySerializer.fromBytes((Key) key, commandsForKeyTable.iterator().next().getBytes("data"));
assertEquals(txnIds.length, cfk.size());
for (int i = 0 ; i < txnIds.length ; ++i)
for (int i = 0; i < txnIds.length; ++i)
assertEquals(txnIds[i], cfk.txnId(i));
test.test(commandStore);
}
// This little bit of magic is required because we do not expose range commands explicitly, but still need to compact them
private static BiConsumer<Command, Command> appendDiffToKeyspace(AccordCommandStore commandStore)
{
return (before, after) -> {
AccordKeyspace.getCommandMutation(commandStore.id(), before, after, commandStore.nextSystemTimestampMicros()).applyUnsafe();
};
}
private List<Partition> compactCFS(IAccordService mockAccordService, ColumnFamilyStore cfs)
{
List<ISSTableScanner> scanners = cfs.getLiveSSTables().stream().map(SSTableReader::getScanner).collect(Collectors.toList());

View File

@ -47,16 +47,8 @@ public class JournalTest
File directory = new File(Files.createTempDirectory("JournalTest"));
directory.deleteRecursiveOnExit();
AsyncCallbacks<TimeUUID, Long> callbacks = new AsyncCallbacks<>()
{
@Override public void onWrite(long segment, int position, int size, TimeUUID key, Long value, Object writeContext) {}
@Override public void onWriteFailed(TimeUUID key, Long value, Object writeContext, Throwable cause) {}
@Override public void onFlush(long segment, int position) {}
@Override public void onFlushFailed(Throwable cause) {}
};
Journal<TimeUUID, Long> journal =
new Journal<>("TestJournal", directory, TestParams.INSTANCE, callbacks, TimeUUIDKeySupport.INSTANCE, LongSerializer.INSTANCE);
new Journal<>("TestJournal", directory, TestParams.INSTANCE, TimeUUIDKeySupport.INSTANCE, LongSerializer.INSTANCE);
journal.start();
@ -65,10 +57,10 @@ public class JournalTest
TimeUUID id3 = nextTimeUUID();
TimeUUID id4 = nextTimeUUID();
journal.write(id1, 1L, Collections.singleton(1));
journal.write(id2, 2L, Collections.singleton(1));
journal.write(id3, 3L, Collections.singleton(1));
journal.write(id4, 4L, Collections.singleton(1));
journal.blockingWrite(id1, 1L, Collections.singleton(1));
journal.blockingWrite(id2, 2L, Collections.singleton(1));
journal.blockingWrite(id3, 3L, Collections.singleton(1));
journal.blockingWrite(id4, 4L, Collections.singleton(1));
assertEquals(1L, (long) journal.readFirst(id1));
assertEquals(2L, (long) journal.readFirst(id2));
@ -77,7 +69,7 @@ public class JournalTest
journal.shutdown();
journal = new Journal<>("TestJournal", directory, TestParams.INSTANCE, callbacks, TimeUUIDKeySupport.INSTANCE, LongSerializer.INSTANCE);
journal = new Journal<>("TestJournal", directory, TestParams.INSTANCE, TimeUUIDKeySupport.INSTANCE, LongSerializer.INSTANCE);
journal.start();
assertEquals(1L, (long) journal.readFirst(id1));

View File

@ -21,7 +21,7 @@ import org.apache.cassandra.net.MessagingService;
public class TestParams implements Params
{
static final TestParams INSTANCE = new TestParams();
public static final TestParams INSTANCE = new TestParams();
@Override
public int segmentSize()

View File

@ -31,18 +31,16 @@ import org.slf4j.LoggerFactory;
import accord.api.Key;
import accord.api.Result;
import accord.local.CommandsForKey;
import accord.impl.TimestampsForKeys;
import accord.impl.TimestampsForKey;
import accord.impl.TimestampsForKeys;
import accord.local.Command;
import accord.local.CommandsForKey;
import accord.local.CommonAttributes;
import accord.local.SaveStatus;
import accord.messages.Apply;
import accord.primitives.Ballot;
import accord.primitives.PartialDeps;
import accord.primitives.PartialTxn;
import accord.primitives.Range;
import accord.primitives.Ranges;
import accord.primitives.Routable;
import accord.primitives.Route;
import accord.primitives.RoutingKeys;
@ -104,7 +102,7 @@ public class AccordCommandStoreTest
{
AtomicLong clock = new AtomicLong(0);
PartialTxn depTxn = createPartialTxn(0);
Key key = (Key)depTxn.keys().get(0);
Key key = (Key) depTxn.keys().get(0);
Range range = key.toUnseekable().asRange();
AccordCommandStore commandStore = createAccordCommandStore(clock::incrementAndGet, "ks", "tbl");
@ -139,31 +137,19 @@ public class AccordCommandStoreTest
attrs.addListener(new Command.ProxyListener(oldTxnId1));
Pair<Writes, Result> result = AccordTestUtils.processTxnResult(commandStore, txnId, txn, executeAt);
Command command = Command.SerializerSupport.executed(attrs, SaveStatus.Applied, executeAt, promised, accepted,
waitingOn, result.left, CommandSerializers.APPLIED);
Command expected = Command.SerializerSupport.executed(attrs, SaveStatus.Applied, executeAt, promised, accepted,
waitingOn, result.left, CommandSerializers.APPLIED);
AccordSafeCommand safeCommand = new AccordSafeCommand(loaded(txnId, null));
safeCommand.set(command);
safeCommand.set(expected);
Apply apply =
Apply.SerializationSupport.create(txnId,
route.slice(Ranges.of(TokenRange.fullRange(tableId))),
1L,
Apply.Kind.Maximal,
depTxn.keys(),
executeAt,
dependencies,
txn,
null,
result.left,
CommandSerializers.APPLIED);
commandStore.appendToJournal(apply);
AccordTestUtils.appendCommandsBlocking(commandStore, null, expected);
AccordKeyspace.getCommandMutation(commandStore, safeCommand, commandStore.nextSystemTimestampMicros()).apply();
logger.info("E: {}", command);
Command actual = AccordKeyspace.loadCommand(commandStore, txnId);
logger.info("E: {}", expected);
Command actual = commandStore.loadCommand(txnId);
logger.info("A: {}", actual);
Assert.assertEquals(command, actual);
Assert.assertEquals(expected, actual);
}
@Test

View File

@ -31,6 +31,8 @@ import accord.local.Command;
import accord.local.KeyHistory;
import accord.local.Node;
import accord.local.PreLoadContext;
import accord.local.SafeCommand;
import accord.local.SafeCommandStore;
import accord.local.Status;
import accord.messages.Accept;
import accord.messages.Commit;
@ -41,6 +43,7 @@ import accord.primitives.Keys;
import accord.primitives.PartialDeps;
import accord.primitives.PartialRoute;
import accord.primitives.PartialTxn;
import accord.primitives.Route;
import accord.primitives.Timestamp;
import accord.primitives.Txn;
import accord.primitives.TxnId;
@ -100,25 +103,33 @@ public class AccordCommandTest
PartialRoute<?> route = fullRoute.slice(fullRange(txn));
PartialTxn partialTxn = txn.slice(route.covering(), true);
PreAccept preAccept = PreAccept.SerializerSupport.create(txnId, route, 1, 1, false, 1, partialTxn, fullRoute);
commandStore.appendToJournal(preAccept);
// Check preaccept
getUninterruptibly(commandStore.execute(preAccept, instance -> {
PreAccept.PreAcceptReply reply = preAccept.apply(instance);
getUninterruptibly(commandStore.execute(preAccept, safeStore -> {
SafeCommand safeCommand = safeStore.get(txnId, txnId, route);
Command before = safeCommand.current();
PreAccept.PreAcceptReply reply = preAccept.apply(safeStore);
Command after = safeCommand.current();
Assert.assertTrue(reply.isOk());
PreAccept.PreAcceptOk ok = (PreAccept.PreAcceptOk) reply;
Assert.assertEquals(txnId, ok.witnessedAt);
Assert.assertTrue(ok.deps.isEmpty());
AccordTestUtils.appendCommandsBlocking(commandStore, before, after);
}));
getUninterruptibly(commandStore.execute(preAccept, instance -> {
Command command = instance.ifInitialised(txnId).current();
Assert.assertEquals(txnId, command.executeAt());
Assert.assertEquals(Status.PreAccepted, command.status());
Assert.assertTrue(command.partialDeps() == null || command.partialDeps().isEmpty());
getUninterruptibly(commandStore.execute(preAccept, safeStore -> {
Command before = safeStore.ifInitialised(txnId).current();
SafeCommand safeCommand = safeStore.get(txnId, txnId, route);
Assert.assertEquals(txnId, before.executeAt());
Assert.assertEquals(Status.PreAccepted, before.status());
Assert.assertTrue(before.partialDeps() == null || before.partialDeps().isEmpty());
CommandsForKey cfk = ((AccordSafeCommandStore) instance).get(key(1)).current();
CommandsForKey cfk = safeStore.get(key(1)).current();
Assert.assertTrue(cfk.indexOf(txnId) >= 0);
Command after = safeCommand.current();
AccordTestUtils.appendCommandsBlocking(commandStore, before, after);
}));
// check accept
@ -131,37 +142,42 @@ public class AccordCommandTest
deps = builder.build();
}
Accept accept = Accept.SerializerSupport.create(txnId, route, 1, 1, false, Ballot.ZERO, executeAt, partialTxn.keys(), deps);
commandStore.appendToJournal(accept);
getUninterruptibly(commandStore.execute(accept, instance -> {
Accept.AcceptReply reply = accept.apply(instance);
getUninterruptibly(commandStore.execute(accept, safeStore -> {
Command before = safeStore.ifInitialised(txnId).current();
Accept.AcceptReply reply = accept.apply(safeStore);
Assert.assertTrue(reply.isOk());
Assert.assertTrue(reply.deps.isEmpty());
Command after = safeStore.ifInitialised(txnId).current();
AccordTestUtils.appendCommandsBlocking(commandStore, before, after);
}));
getUninterruptibly(commandStore.execute(accept, instance -> {
Command command = instance.ifInitialised(txnId).current();
Assert.assertEquals(executeAt, command.executeAt());
Assert.assertEquals(Status.Accepted, command.status());
Assert.assertEquals(deps, command.partialDeps());
getUninterruptibly(commandStore.execute(accept, safeStore -> {
Command before = safeStore.ifInitialised(txnId).current();
Assert.assertEquals(executeAt, before.executeAt());
Assert.assertEquals(Status.Accepted, before.status());
Assert.assertEquals(deps, before.partialDeps());
CommandsForKey cfk = ((AccordSafeCommandStore) instance).get(key(1)).current();
CommandsForKey cfk = safeStore.get(key(1)).current();
Assert.assertTrue(cfk.indexOf(txnId) >= 0);
Command after = safeStore.ifInitialised(txnId).current();
AccordTestUtils.appendCommandsBlocking(commandStore, before, after);
}));
// check commit
Commit commit = Commit.SerializerSupport.create(txnId, route, 1, Commit.Kind.StableWithTxnAndDeps, Ballot.ZERO, executeAt, partialTxn.keys(), partialTxn, deps, fullRoute, null);
commandStore.appendToJournal(commit);
getUninterruptibly(commandStore.execute(commit, commit::apply));
getUninterruptibly(commandStore.execute(PreLoadContext.contextFor(txnId, Keys.of(key), KeyHistory.COMMANDS), instance -> {
Command command = instance.ifInitialised(txnId).current();
Assert.assertEquals(commit.executeAt, command.executeAt());
Assert.assertTrue(command.hasBeen(Status.Committed));
Assert.assertEquals(commit.partialDeps, command.partialDeps());
getUninterruptibly(commandStore.execute(PreLoadContext.contextFor(txnId, Keys.of(key), KeyHistory.COMMANDS), safeStore -> {
Command before = safeStore.ifInitialised(txnId).current();
Assert.assertEquals(commit.executeAt, before.executeAt());
Assert.assertTrue(before.hasBeen(Status.Committed));
Assert.assertEquals(commit.partialDeps, before.partialDeps());
CommandsForKey cfk = ((AccordSafeCommandStore) instance).get(key(1)).current();
CommandsForKey cfk = safeStore.get(key(1)).current();
Assert.assertTrue(cfk.indexOf(txnId) >= 0);
Command after = safeStore.ifInitialised(txnId).current();
AccordTestUtils.appendCommandsBlocking(commandStore, before, after);
}));
}
@ -179,19 +195,32 @@ public class AccordCommandTest
PartialRoute<?> route = fullRoute.slice(fullRange(txn));
PartialTxn partialTxn = txn.slice(route.covering(), true);
PreAccept preAccept1 = PreAccept.SerializerSupport.create(txnId1, route, 1, 1, false, 1, partialTxn, fullRoute);
commandStore.appendToJournal(preAccept1);
getUninterruptibly(commandStore.execute(preAccept1, preAccept1::apply));
getUninterruptibly(commandStore.execute(preAccept1, safeStore -> {
persistDiff(commandStore, safeStore, txnId1, route, () -> {
preAccept1.apply(safeStore);
});
}));
// second preaccept should identify txnId1 as a dependency
TxnId txnId2 = txnId(1, clock.incrementAndGet(), 1);
PreAccept preAccept2 = PreAccept.SerializerSupport.create(txnId2, route, 1, 1, false, 1, partialTxn, fullRoute);
commandStore.appendToJournal(preAccept2);
getUninterruptibly(commandStore.execute(preAccept2, instance -> {
PreAccept.PreAcceptReply reply = preAccept2.apply(instance);
Assert.assertTrue(reply.isOk());
PreAccept.PreAcceptOk ok = (PreAccept.PreAcceptOk) reply;
Assert.assertTrue(ok.deps.contains(txnId1));
getUninterruptibly(commandStore.execute(preAccept2, safeStore -> {
persistDiff(commandStore, safeStore, txnId2, route, () -> {
PreAccept.PreAcceptReply reply = preAccept2.apply(safeStore);
Assert.assertTrue(reply.isOk());
PreAccept.PreAcceptOk ok = (PreAccept.PreAcceptOk) reply;
Assert.assertTrue(ok.deps.contains(txnId1));
});
}));
}
private static void persistDiff(AccordCommandStore commandStore, SafeCommandStore safeStore, TxnId txnId, Route<?> route, Runnable runnable)
{
SafeCommand safeCommand = safeStore.get(txnId, txnId, route);
Command before = safeCommand.current();
runnable.run();
Command after = safeCommand.current();
AccordTestUtils.appendCommandsBlocking(commandStore, before, after);
}
}

View File

@ -0,0 +1,110 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cassandra.service.accord;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import accord.primitives.TxnId;
import accord.utils.AccordGens;
import accord.utils.RandomSource;
import org.apache.cassandra.SchemaLoader;
import org.apache.cassandra.ServerTestUtils;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.journal.TestParams;
import org.apache.cassandra.schema.KeyspaceParams;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.service.consensus.TransactionalMode;
import org.apache.cassandra.utils.StorageCompatibilityMode;
import static org.apache.cassandra.cql3.statements.schema.CreateTableStatement.parse;
public class AccordJournalOrderTest
{
@BeforeClass
public static void beforeClass() throws Throwable
{
CassandraRelevantProperties.JUNIT_STORAGE_COMPATIBILITY_MODE.setEnum(StorageCompatibilityMode.NONE);
SchemaLoader.prepareServer();
SchemaLoader.createKeyspace("ks", KeyspaceParams.simple(1),
parse("CREATE TABLE tbl (k int, c int, v int, primary key (k, c)) WITH transactional_mode='full'", "ks"));
TableMetadata tbl = Schema.instance.getTableMetadata("ks", "tbl");
Assert.assertEquals(TransactionalMode.full, tbl.params.transactionalMode);
StorageService.instance.initServer();
}
@Test
public void simpleKeyTest()
{
if (new File(DatabaseDescriptor.getAccordJournalDirectory()).exists())
ServerTestUtils.cleanupDirectory(DatabaseDescriptor.getAccordJournalDirectory());
AccordJournal accordJournal = new AccordJournal(SimpleAccordEndpointMapper.INSTANCE, TestParams.INSTANCE);
accordJournal.start(null);
RandomSource randomSource = RandomSource.wrap(new Random());
TxnId id1 = AccordGens.txnIds().next(randomSource);
TxnId id2 = AccordGens.txnIds().next(randomSource);
Map<JournalKey, Integer> res = new HashMap<>();
for (int i = 0; i < 10_000; i++)
{
TxnId txnId = randomSource.nextBoolean() ? id1 : id2;
JournalKey key = new JournalKey(txnId, AccordJournal.Type.SAVED_COMMAND, randomSource.nextInt(5));
res.compute(key, (k, prev) -> prev == null ? 1 : prev + 1);
accordJournal.appendCommand(key.commandStoreId,
Collections.singletonList(new SavedCommand.SavedDiff(txnId,
AccordGens.timestamps().next(randomSource),
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null)),
null,
() -> {});
}
Runnable check = () -> {
for (JournalKey key : res.keySet())
{
List<SavedCommand.LoadedDiff> diffs = accordJournal.loadDiffs(key.commandStoreId, key.timestamp);
Assert.assertEquals(diffs.size(), res.get(key).intValue());
}
};
check.run();
accordJournal.closeCurrentSegmentForTesting();
check.run();
}
}

View File

@ -33,7 +33,6 @@ import accord.utils.Gens;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.io.util.DataInputBuffer;
import org.apache.cassandra.io.util.DataOutputBuffer;
import org.apache.cassandra.service.accord.AccordJournal.Key;
import org.apache.cassandra.utils.AsymmetricOrdering;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.FBUtilities.Order;
@ -58,12 +57,12 @@ public class AccordJournalTest
qt().forAll(keyGen()).check(key ->
{
buffer.clear();
int expectedSize = Key.SUPPORT.serializedSize(1);
Key.SUPPORT.serialize(key, buffer, 1);
int expectedSize = JournalKey.SUPPORT.serializedSize(1);
JournalKey.SUPPORT.serialize(key, buffer, 1);
assertThat(buffer.getLength()).isEqualTo(expectedSize);
try (DataInputBuffer input = new DataInputBuffer(buffer.unsafeGetBufferAndFlip(), false))
{
Key read = Key.SUPPORT.deserialize(input, 1);
JournalKey read = JournalKey.SUPPORT.deserialize(input, 1);
assertThat(read).isEqualTo(key);
}
});
@ -74,29 +73,29 @@ public class AccordJournalTest
{
qt().forAll(Gens.lists(keyGen()).ofSizeBetween(2, 100)).check(keys ->
{
keys.sort(Key.SUPPORT);
keys.sort(JournalKey.SUPPORT);
List<ByteBuffer> buffers = new ArrayList<>(keys.size());
for (Key k : keys) buffers.add(toBuffer(k));
for (JournalKey k : keys) buffers.add(toBuffer(k));
for (int i = 0; i < keys.size(); i++)
{
Key outerKey = keys.get(i);
JournalKey outerKey = keys.get(i);
for (int j = 0; j < keys.size(); j++)
{
Key innerKey = keys.get(j);
JournalKey innerKey = keys.get(j);
ByteBuffer innerBuffer = buffers.get(j);
Order expected = FBUtilities.compare(outerKey, innerKey, Key.SUPPORT);
Order actual = FBUtilities.compare(outerKey, innerBuffer, new AsymmetricOrdering<Key, ByteBuffer>()
Order expected = FBUtilities.compare(outerKey, innerKey, JournalKey.SUPPORT);
Order actual = FBUtilities.compare(outerKey, innerBuffer, new AsymmetricOrdering<JournalKey, ByteBuffer>()
{
@Override
public int compareAsymmetric(Key left, ByteBuffer right)
public int compareAsymmetric(JournalKey left, ByteBuffer right)
{
return Key.SUPPORT.compareWithKeyAt(left, right, 0, 1);
return JournalKey.SUPPORT.compareWithKeyAt(left, right, 0, 1);
}
@Override
public int compare(@Nullable Key left, @Nullable Key right)
public int compare(@Nullable JournalKey left, @Nullable JournalKey right)
{
throw new UnsupportedOperationException();
}
@ -107,11 +106,11 @@ public class AccordJournalTest
});
}
private static ByteBuffer toBuffer(Key k)
private static ByteBuffer toBuffer(JournalKey k)
{
try (DataOutputBuffer buffer = new DataOutputBuffer(Key.SUPPORT.serializedSize(1)))
try (DataOutputBuffer buffer = new DataOutputBuffer(JournalKey.SUPPORT.serializedSize(1)))
{
Key.SUPPORT.serialize(k, buffer, 1);
JournalKey.SUPPORT.serialize(k, buffer, 1);
return buffer.unsafeGetBufferAndFlip();
}
catch (IOException e)
@ -120,10 +119,10 @@ public class AccordJournalTest
}
}
private Gen<Key> keyGen()
private Gen<JournalKey> keyGen()
{
Gen<TxnId> txnIdGen = AccordGens.txnIds();
Gen<AccordJournal.Type> typeGen = Gens.enums().all(AccordJournal.Type.class);
return rs -> new Key(txnIdGen.next(rs), typeGen.next(rs));
return rs -> new JournalKey(txnIdGen.next(rs), typeGen.next(rs));
}
}

View File

@ -37,13 +37,11 @@ import accord.local.CommonAttributes;
import accord.local.Node;
import accord.local.SaveStatus;
import accord.local.Status;
import accord.messages.Commit;
import accord.primitives.Ballot;
import accord.primitives.Deps;
import accord.primitives.FullRoute;
import accord.primitives.KeyDeps;
import accord.primitives.Keys;
import accord.primitives.PartialDeps;
import accord.primitives.PartialTxn;
import accord.primitives.RangeDeps;
import accord.primitives.Ranges;
@ -97,7 +95,7 @@ public class AccordKeyspaceTest extends CQLTester.InMemory
{
AtomicLong now = new AtomicLong();
String tableName = createTable("CREATE TABLE %s (k int, c int, v int, PRIMARY KEY (k, c))");
String tableName = createTable("CREATE TABLE %s (k int, c int, v int, PRIMARY KEY (k, c)) WITH transactional_mode = 'full'");
TableId tableId = Schema.instance.getTableMetadata(KEYSPACE, tableName).id;
Ranges scope = Ranges.of(new TokenRange(AccordRoutingKey.SentinelKey.min(tableId), AccordRoutingKey.SentinelKey.max(tableId)));
@ -111,8 +109,7 @@ public class AccordKeyspaceTest extends CQLTester.InMemory
RoutingKey routingKey = partialTxn.keys().get(0).asKey().toUnseekable();
FullRoute<?> route = partialTxn.keys().toRoute(routingKey);
Deps deps = new Deps(KeyDeps.none((Keys) txn.keys()), RangeDeps.NONE);
PartialDeps partialDeps = deps.slice(scope);
deps.slice(scope);
CommonAttributes.Mutable common = new CommonAttributes.Mutable(id);
common.partialTxn(partialTxn);
@ -125,13 +122,12 @@ public class AccordKeyspaceTest extends CQLTester.InMemory
AccordSafeCommand safeCommand = new AccordSafeCommand(AccordTestUtils.loaded(id, null));
safeCommand.set(committed);
Commit commit = Commit.SerializerSupport.create(id, route.slice(scope), 1, Commit.Kind.CommitSlowPath, Ballot.ZERO, id, partialTxn.keys(), partialTxn, partialDeps, route, null);
store.appendToJournal(commit);
AccordTestUtils.appendCommandsBlocking(store, null, committed);
Mutation mutation = AccordKeyspace.getCommandMutation(store, safeCommand, 42);
mutation.apply();
Command loaded = AccordKeyspace.loadCommand(store, id);
Command loaded = store.loadCommand(id);
Assertions.assertThat(loaded).isEqualTo(committed);
}

View File

@ -34,6 +34,9 @@ import javax.annotation.Nullable;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.cassandra.ServerTestUtils;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.io.util.File;
import org.junit.Assert;
import accord.api.Data;
@ -95,6 +98,7 @@ import org.apache.cassandra.service.accord.api.PartitionKey;
import org.apache.cassandra.service.accord.txn.TxnData;
import org.apache.cassandra.service.accord.txn.TxnRead;
import org.apache.cassandra.utils.Pair;
import org.apache.cassandra.utils.concurrent.Condition;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import static accord.primitives.Routable.Domain.Key;
@ -400,6 +404,9 @@ public class AccordTestUtils
public long unix(TimeUnit timeUnit) { return NodeTimeService.unixWrapper(TimeUnit.MICROSECONDS, this::now).applyAsLong(timeUnit); }
};
if (new File(DatabaseDescriptor.getAccordJournalDirectory()).exists())
ServerTestUtils.cleanupDirectory(DatabaseDescriptor.getAccordJournalDirectory());
AccordJournal journal = new AccordJournal(null, new AccordSpec.JournalSpec());
journal.start(null);
@ -498,4 +505,19 @@ public class AccordTestUtils
return range(token(left), token(right));
}
public static void appendCommandsBlocking(AccordCommandStore commandStore, Command after)
{
appendCommandsBlocking(commandStore, null, after);
}
public static void appendCommandsBlocking(AccordCommandStore commandStore, Command before, Command after)
{
SavedCommand.SavedDiff diff = SavedCommand.diff(before, after);
if (diff != null)
{
Condition condition = Condition.newOneTimeCondition();
commandStore.appendCommands(Collections.singletonList(diff), null, condition::signal);
condition.awaitUninterruptibly(30, TimeUnit.SECONDS);
}
}
}

View File

@ -18,199 +18,52 @@
package org.apache.cassandra.service.accord;
import java.util.EnumSet;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.common.collect.Sets;
import accord.local.SerializerSupport;
import accord.messages.Accept;
import accord.messages.Apply;
import accord.messages.ApplyThenWaitUntilApplied;
import accord.messages.BeginRecovery;
import accord.messages.Commit;
import accord.messages.Message;
import accord.messages.MessageType;
import accord.messages.PreAccept;
import accord.messages.Propagate;
import accord.primitives.Ballot;
import accord.local.Command;
import accord.primitives.TxnId;
import org.agrona.collections.ObjectHashSet;
import org.apache.cassandra.service.accord.AccordJournal.Key;
import org.apache.cassandra.service.accord.AccordJournal.Type;
import static accord.messages.MessageType.ACCEPT_REQ;
import static accord.messages.MessageType.APPLY_MAXIMAL_REQ;
import static accord.messages.MessageType.APPLY_MINIMAL_REQ;
import static accord.messages.MessageType.APPLY_THEN_WAIT_UNTIL_APPLIED_REQ;
import static accord.messages.MessageType.BEGIN_RECOVER_REQ;
import static accord.messages.MessageType.COMMIT_MAXIMAL_REQ;
import static accord.messages.MessageType.COMMIT_SLOW_PATH_REQ;
import static accord.messages.MessageType.PRE_ACCEPT_REQ;
import static accord.messages.MessageType.PROPAGATE_APPLY_MSG;
import static accord.messages.MessageType.PROPAGATE_OTHER_MSG;
import static accord.messages.MessageType.PROPAGATE_PRE_ACCEPT_MSG;
import static accord.messages.MessageType.PROPAGATE_STABLE_MSG;
import static accord.messages.MessageType.STABLE_FAST_PATH_REQ;
import static accord.messages.MessageType.STABLE_MAXIMAL_REQ;
import static accord.messages.MessageType.STABLE_SLOW_PATH_REQ;
public class MockJournal implements IJournal
{
private final Map<Key, Message> writes = new HashMap<>();
private final Map<JournalKey, List<SavedCommand.LoadedDiff>> commands = new HashMap<>();
@Override
public SerializerSupport.MessageProvider makeMessageProvider(TxnId txnId)
public Command loadCommand(int commandStoreId, TxnId txnId)
{
return new SerializerSupport.MessageProvider()
{
@Override
public TxnId txnId()
{
return txnId;
}
@Override
public Set<MessageType> test(Set<MessageType> messages)
{
Set<Key> keys = new ObjectHashSet<>(messages.size() + 1, 0.9f);
for (MessageType message : messages)
for (Type synonymousType : Type.synonymousTypesFromMessageType(message))
keys.add(new Key(txnId, synonymousType));
Set<Key> presentKeys = Sets.intersection(writes.keySet(), keys);
Set<MessageType> presentMessages = new ObjectHashSet<>(presentKeys.size() + 1, 0.9f);
for (Key key : presentKeys)
presentMessages.add(key.type.outgoingType);
return presentMessages;
}
@Override
public Set<MessageType> all()
{
Set<Type> types = EnumSet.allOf(Type.class);
Set<Key> keys = new ObjectHashSet<>(types.size() + 1, 0.9f);
for (Type type : types)
keys.add(new Key(txnId, type));
Set<Key> presentKeys = Sets.intersection(writes.keySet(), keys);
Set<MessageType> presentMessages = new ObjectHashSet<>(presentKeys.size() + 1, 0.9f);
for (Key key : presentKeys)
presentMessages.add(key.type.outgoingType);
return presentMessages;
}
private <T extends Message> T get(Key key)
{
return (T) writes.get(key);
}
private <T extends Message> T get(MessageType messageType)
{
for (Type type : Type.synonymousTypesFromMessageType(messageType))
{
T value = get(new Key(txnId, type));
if (value != null) return value;
}
return null;
}
@Override
public PreAccept preAccept()
{
return get(PRE_ACCEPT_REQ);
}
@Override
public BeginRecovery beginRecover()
{
return get(BEGIN_RECOVER_REQ);
}
@Override
public Propagate propagatePreAccept()
{
return get(PROPAGATE_PRE_ACCEPT_MSG);
}
@Override
public Accept accept(Ballot ballot)
{
return get(ACCEPT_REQ);
}
@Override
public Commit commitSlowPath()
{
return get(COMMIT_SLOW_PATH_REQ);
}
@Override
public Commit commitMaximal()
{
return get(COMMIT_MAXIMAL_REQ);
}
@Override
public Commit stableFastPath()
{
return get(STABLE_FAST_PATH_REQ);
}
@Override
public Commit stableSlowPath()
{
return get(STABLE_SLOW_PATH_REQ);
}
@Override
public Commit stableMaximal()
{
return get(STABLE_MAXIMAL_REQ);
}
@Override
public Propagate propagateStable()
{
return get(PROPAGATE_STABLE_MSG);
}
@Override
public Apply applyMinimal()
{
return get(APPLY_MINIMAL_REQ);
}
@Override
public Apply applyMaximal()
{
return get(APPLY_MAXIMAL_REQ);
}
@Override
public Propagate propagateApply()
{
return get(PROPAGATE_APPLY_MSG);
}
@Override
public Propagate propagateOther()
{
return get(PROPAGATE_OTHER_MSG);
}
@Override
public ApplyThenWaitUntilApplied applyThenWaitUntilApplied()
{
return get(APPLY_THEN_WAIT_UNTIL_APPLIED_REQ);
}
};
Type type = Type.SAVED_COMMAND;
JournalKey key = new JournalKey(txnId, type, commandStoreId);
List<SavedCommand.LoadedDiff> saved = commands.get(key);
if (saved == null)
return null;
return SavedCommand.reconstructFromDiff(new ArrayList<>(saved));
}
@Override
public void appendMessageBlocking(Message message)
public void appendCommand(int commandStoreId, List<SavedCommand.SavedDiff> diffs, List<Command> sanityCheck, Runnable onFlush)
{
Type type = Type.fromMessageType(message.type());
Key key = new Key(type.txnId(message), type);
writes.put(key, message);
Type type = Type.SAVED_COMMAND;
for (SavedCommand.SavedDiff diff : diffs)
{
JournalKey key = new JournalKey(diff.txnId, type, commandStoreId);
commands.computeIfAbsent(key, (ignore_) -> new ArrayList<>())
.add(new SavedCommand.LoadedDiff(diff.txnId,
diff.executeAt,
diff.saveStatus,
diff.durability,
diff.acceptedOrCommitted,
diff.promised,
diff.route,
diff.partialTxn,
diff.partialDeps,
diff.additionalKeysOrRanges,
(i1, i2) -> diff.waitingOn,
diff.writes,
diff.listeners));
}
onFlush.run();
}
}

View File

@ -38,7 +38,6 @@ import accord.local.NodeTimeService;
import accord.local.PreLoadContext;
import accord.local.SafeCommandStore;
import accord.messages.BeginRecovery;
import accord.messages.Message;
import accord.messages.PreAccept;
import accord.messages.TxnRequest;
import accord.primitives.Ballot;
@ -332,8 +331,6 @@ public class SimulatedAccordCommandStore implements AutoCloseable
public <T> AsyncResult<T> processAsync(PreLoadContext loadCtx, Function<? super SafeCommandStore, T> function)
{
if (loadCtx instanceof Message)
journal.appendMessageBlocking((Message) loadCtx);
return store.submit(loadCtx, function).beginAsResult();
}

View File

@ -26,6 +26,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Ignore;
import org.junit.Test;
import accord.api.Key;
@ -46,6 +47,7 @@ import org.apache.cassandra.service.accord.api.PartitionKey;
import static accord.utils.Property.qt;
import static org.apache.cassandra.service.accord.AccordTestUtils.createTxn;
@Ignore // TODO (required): This class relies on removed ExecutionOrder for correctness, and needs to be adjusted
public class SimulatedDepsTest extends SimulatedAccordCommandStoreTestBase
{
@Test

View File

@ -29,6 +29,7 @@ import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.junit.Ignore;
import org.junit.Test;
import accord.api.Key;
@ -52,6 +53,7 @@ import static accord.utils.Property.qt;
import static org.apache.cassandra.dht.Murmur3Partitioner.LongToken.keyForToken;
import static org.apache.cassandra.service.accord.AccordTestUtils.createTxn;
@Ignore // TODO (required): This class relies on removed ExecutionOrder for correctness, and needs to be adjusted
public class SimulatedMultiKeyAndRangeTest extends SimulatedAccordCommandStoreTestBase
{
@Test

View File

@ -25,6 +25,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Ignore;
import org.junit.Test;
import accord.api.Key;
@ -41,6 +42,7 @@ import static accord.utils.Property.qt;
import static org.apache.cassandra.dht.Murmur3Partitioner.LongToken.keyForToken;
import static org.apache.cassandra.service.accord.AccordTestUtils.createTxn;
@Ignore // TODO (required): This class relies on removed ExecutionOrder for correctness, and needs to be adjusted
public class SimulatedRandomKeysWithRangeConflictTest extends SimulatedAccordCommandStoreTestBase
{
@Test

View File

@ -24,6 +24,7 @@ import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import accord.utils.DefaultRandom;
@ -47,9 +48,6 @@ import accord.local.PreLoadContext;
import accord.local.SafeCommand;
import accord.local.SafeCommandStore;
import accord.local.SaveStatus;
import accord.messages.Accept;
import accord.messages.Commit;
import accord.messages.PreAccept;
import accord.primitives.Ballot;
import accord.primitives.FullRoute;
import accord.primitives.Keys;
@ -57,7 +55,6 @@ import accord.primitives.PartialDeps;
import accord.primitives.PartialRoute;
import accord.primitives.PartialTxn;
import accord.primitives.Ranges;
import accord.primitives.Route;
import accord.primitives.Timestamp;
import accord.primitives.Txn;
import accord.primitives.TxnId;
@ -84,6 +81,7 @@ import org.apache.cassandra.service.accord.AccordTestUtils;
import org.apache.cassandra.service.accord.api.PartitionKey;
import org.apache.cassandra.utils.AssertionUtils;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.concurrent.Condition;
import org.assertj.core.api.Assertions;
import org.awaitility.Awaitility;
import org.mockito.Mockito;
@ -178,20 +176,7 @@ public class AsyncOperationTest
safeCommand.set(command);
AccordKeyspace.getCommandMutation(commandStore, safeCommand, commandStore.nextSystemTimestampMicros()).apply();
Commit commit =
Commit.SerializerSupport.create(txnId,
command.route().slice(AccordTestUtils.fullRange(command.partialTxn().keys())),
txnId.epoch(),
Commit.Kind.StableWithTxnAndDeps,
Ballot.ZERO,
executeAt,
command.partialTxn().keys(),
command.partialTxn(),
command.partialDeps(),
Route.castToFullRoute(command.route()),
null);
commandStore.appendToJournal(commit);
appendDiffToLog(commandStore).accept(null, command);
return command;
}
@ -211,23 +196,14 @@ public class AsyncOperationTest
RoutingKey routingKey = partialTxn.keys().get(0).asKey().toUnseekable();
FullRoute<?> route = partialTxn.keys().toRoute(routingKey);
Ranges ranges = AccordTestUtils.fullRange(partialTxn.keys());
PartialRoute<?> partialRoute = route.slice(ranges);
route.slice(ranges);
PartialDeps deps = PartialDeps.builder(ranges).build();
// create and write messages to the journal for loading to succeed
PreAccept preAccept =
PreAccept.SerializerSupport.create(txnId, partialRoute, txnId.epoch(), txnId.epoch(), false, txnId.epoch(), partialTxn, route);
Commit stable =
Commit.SerializerSupport.create(txnId, partialRoute, txnId.epoch(), Commit.Kind.StableFastPath, Ballot.ZERO, executeAt, partialTxn.keys(), partialTxn, deps, route, null);
commandStore.appendToJournal(preAccept);
commandStore.appendToJournal(stable);
try
{
Command command = getUninterruptibly(commandStore.submit(contextFor(txnId, partialTxn.keys(), COMMANDS), safe -> {
CheckedCommands.preaccept(safe, txnId, partialTxn, route, null);
CheckedCommands.commit(safe, SaveStatus.Stable, Ballot.ZERO, txnId, route, null, partialTxn, executeAt, deps);
CheckedCommands.preaccept(safe, txnId, partialTxn, route, null, appendDiffToLog(commandStore));
CheckedCommands.commit(safe, SaveStatus.Stable, Ballot.ZERO, txnId, route, null, partialTxn, executeAt, deps, appendDiffToLog(commandStore));
return safe.ifInitialised(txnId).current();
}).beginAsResult());
@ -252,6 +228,15 @@ public class AsyncOperationTest
return createStableUsingSlowLifeCycle(commandStore, txnId, txnId);
}
private static BiConsumer<Command, Command> appendDiffToLog(AccordCommandStore commandStore)
{
return (before, after) -> {
Condition condition = Condition.newOneTimeCondition();
commandStore.appendToLog(before, after, condition::signal);
condition.awaitUninterruptibly();
};
}
private static Command createStableUsingSlowLifeCycle(AccordCommandStore commandStore, TxnId txnId, Timestamp executeAt)
{
PartialTxn partialTxn = createPartialTxn(0);
@ -261,28 +246,13 @@ public class AsyncOperationTest
PartialRoute<?> partialRoute = route.slice(ranges);
PartialDeps deps = PartialDeps.builder(ranges).build();
// create and write messages to the journal for loading to succeed
PreAccept preAccept =
PreAccept.SerializerSupport.create(txnId, partialRoute, txnId.epoch(), txnId.epoch(), false, txnId.epoch(), partialTxn, route);
Accept accept =
Accept.SerializerSupport.create(txnId, partialRoute, txnId.epoch(), txnId.epoch(), false, Ballot.ZERO, executeAt, partialTxn.keys(), deps);
Commit commit =
Commit.SerializerSupport.create(txnId, partialRoute, txnId.epoch(), Commit.Kind.CommitSlowPath, Ballot.ZERO, executeAt, partialTxn.keys(), partialTxn, deps, route, null);
Commit stable =
Commit.SerializerSupport.create(txnId, partialRoute, txnId.epoch(), Commit.Kind.StableSlowPath, Ballot.ZERO, executeAt, partialTxn.keys(), partialTxn, deps, route, null);
commandStore.appendToJournal(preAccept);
commandStore.appendToJournal(accept);
commandStore.appendToJournal(commit);
commandStore.appendToJournal(stable);
try
{
Command command = getUninterruptibly(commandStore.submit(contextFor(txnId, partialTxn.keys(), COMMANDS), safe -> {
CheckedCommands.preaccept(safe, txnId, partialTxn, route, null);
CheckedCommands.accept(safe, txnId, Ballot.ZERO, partialRoute, partialTxn.keys(), null, executeAt, deps);
CheckedCommands.commit(safe, SaveStatus.Committed, Ballot.ZERO, txnId, route, null, partialTxn, executeAt, deps);
CheckedCommands.commit(safe, SaveStatus.Stable, Ballot.ZERO, txnId, route, null, partialTxn, executeAt, deps);
CheckedCommands.preaccept(safe, txnId, partialTxn, route, null, appendDiffToLog(commandStore));
CheckedCommands.accept(safe, txnId, Ballot.ZERO, partialRoute, partialTxn.keys(), null, executeAt, deps, appendDiffToLog(commandStore));
CheckedCommands.commit(safe, SaveStatus.Committed, Ballot.ZERO, txnId, route, null, partialTxn, executeAt, deps, appendDiffToLog(commandStore));
CheckedCommands.commit(safe, SaveStatus.Stable, Ballot.ZERO, txnId, route, null, partialTxn, executeAt, deps, appendDiffToLog(commandStore));
return safe.ifInitialised(txnId).current();
}).beginAsResult());
@ -384,7 +354,10 @@ public class AsyncOperationTest
commandStore.executeBlocking(() -> commandStore.setCapacity(0));
Gen<TxnId> txnIdGen = rs -> txnId(1, clock.incrementAndGet(), 1);
qt().withPure(false).withExamples(50).forAll(Gens.random(), Gens.lists(txnIdGen).ofSizeBetween(1, 10)).check((rs, ids) -> {
qt().withPure(false)
.withSeed(-3537445084098883509L).withExamples(50)
.forAll(Gens.random(), Gens.lists(txnIdGen).ofSizeBetween(1, 10))
.check((rs, ids) -> {
before(); // truncate tables
createCommand(commandStore, rs, ids);
@ -400,7 +373,8 @@ public class AsyncOperationTest
commandStore.commandCache().unsafeSetLoadFunction(txnId ->
{
logger.info("Attempting to load {}; expected to fail? {}", txnId, failed.get(txnId));
if (!failed.get(txnId)) return AccordKeyspace.loadCommand(commandStore, txnId);
if (!failed.get(txnId))
return commandStore.loadCommand(txnId);
throw new NullPointerException("txn_id " + txnId);
});
AsyncOperation<Void> o1 = new AsyncOperation.ForConsumer(commandStore, ctx, consumer);
@ -418,8 +392,15 @@ public class AsyncOperationTest
awaitDone(commandStore, ids, keys);
// can we recover?
commandStore.commandCache().unsafeSetLoadFunction(txnId -> AccordKeyspace.loadCommand(commandStore, txnId));
AsyncOperation.ForConsumer o2 = new AsyncOperation.ForConsumer(commandStore, ctx, store -> ids.forEach(id -> store.ifInitialised(id).readyToExecute(store)));
commandStore.commandCache().unsafeSetLoadFunction(txnId -> {
Command cmd = commandStore.loadCommand(txnId);
return cmd;
});
AsyncOperation.ForConsumer o2 = new AsyncOperation.ForConsumer(commandStore, ctx, store -> {
ids.forEach(id -> {
store.ifInitialised(id).readyToExecute(store);
});
});
getUninterruptibly(o2);
});
}