You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by dc...@apache.org on 2022/10/21 20:14:48 UTC
[cassandra] branch cep-15-accord updated: CEP-15 (C*): Messaging and storage engine integration
This is an automated email from the ASF dual-hosted git repository.
dcapwell pushed a commit to branch cep-15-accord
in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/cep-15-accord by this push:
new e6cbeb39e0 CEP-15 (C*): Messaging and storage engine integration
e6cbeb39e0 is described below
commit e6cbeb39e0dbf4a20798abe828b744f84967cf1f
Author: Blake Eggleston <bl...@ultrablake.com>
AuthorDate: Fri Oct 21 13:10:08 2022 -0700
CEP-15 (C*): Messaging and storage engine integration
patch by Blake Eggleston; reviewed by Benedict Elliott Smith, David Capwell for CASSANDRA-17103
---
.build/cassandra-deps-template.xml | 4 +
.build/include-accord.sh | 58 ++
.build/parent-pom-template.xml | 5 +
.gitignore | 1 +
CHANGES.txt | 3 +
build.xml | 7 +-
simulator.sh | 88 ++
.../concurrent/SingleThreadExecutorPlus.java | 1 +
.../org/apache/cassandra/concurrent/Stage.java | 2 +-
src/java/org/apache/cassandra/config/Config.java | 1 +
.../cassandra/config/DatabaseDescriptor.java | 17 +
.../cql3/statements/ModificationStatement.java | 2 +-
.../apache/cassandra/db/MutableDeletionInfo.java | 10 +
.../apache/cassandra/db/RangeTombstoneList.java | 9 +
.../cassandra/db/SinglePartitionReadCommand.java | 18 +
.../db/filter/AbstractClusteringIndexFilter.java | 1 +
.../db/filter/ClusteringIndexNamesFilter.java | 14 -
.../db/filter/ClusteringIndexSliceFilter.java | 1 +
.../apache/cassandra/db/partitions/Partition.java | 3 +
.../cassandra/db/partitions/PartitionUpdate.java | 17 +
.../org/apache/cassandra/db/rows/AbstractCell.java | 7 +
.../org/apache/cassandra/db/rows/BTreeRow.java | 12 +
.../org/apache/cassandra/db/rows/ColumnData.java | 1 +
.../cassandra/db/rows/ComplexColumnData.java | 7 +
src/java/org/apache/cassandra/db/rows/Row.java | 2 +
.../cassandra/dht/ByteOrderedPartitioner.java | 6 +
.../org/apache/cassandra/dht/LocalPartitioner.java | 6 +
.../apache/cassandra/dht/Murmur3Partitioner.java | 6 +
.../cassandra/dht/OrderPreservingPartitioner.java | 6 +
.../apache/cassandra/dht/RandomPartitioner.java | 6 +
src/java/org/apache/cassandra/dht/Token.java | 1 +
src/java/org/apache/cassandra/metrics/Sampler.java | 1 +
.../org/apache/cassandra/net/ForwardingInfo.java | 1 +
src/java/org/apache/cassandra/net/Message.java | 3 +-
.../apache/cassandra/net/ResponseVerbHandler.java | 2 +-
src/java/org/apache/cassandra/net/Verb.java | 141 +++-
src/java/org/apache/cassandra/schema/Schema.java | 3 +-
.../apache/cassandra/schema/SchemaConstants.java | 3 +-
src/java/org/apache/cassandra/schema/TableId.java | 25 +-
.../cassandra/service/accord/AccordCallback.java | 69 ++
.../cassandra/service/accord/AccordCommand.java | 788 ++++++++++++++++++
.../service/accord/AccordCommandStore.java | 291 +++++++
.../service/accord/AccordCommandStores.java | 65 ++
.../service/accord/AccordCommandsForKey.java | 459 ++++++++++
.../service/accord/AccordConfigurationService.java | 88 ++
.../cassandra/service/accord/AccordKeyspace.java | 843 +++++++++++++++++++
.../service/accord/AccordMessageSink.java | 120 +++
.../service/accord/AccordObjectSizes.java | 101 +++
.../service/accord/AccordPartialCommand.java | 338 ++++++++
.../cassandra/service/accord/AccordService.java | 187 +++++
.../cassandra/service/accord/AccordState.java | 113 +++
.../cassandra/service/accord/AccordStateCache.java | 646 ++++++++++++++
.../service/accord/AccordTopologyUtils.java | 136 +++
.../cassandra/service/accord/AccordTxnBuilder.java | 209 +++++
.../service/accord/AccordVerbHandler.java | 33 +-
.../cassandra/service/accord/EndpointMapping.java | 145 ++++
.../cassandra/service/accord/ListenerProxy.java | 279 +++++++
.../cassandra/service/accord/ReadFuture.java | 295 +++++++
.../cassandra/service/accord/TokenRange.java | 86 ++
.../cassandra/service/accord/api/AccordAgent.java | 31 +-
.../cassandra/service/accord/api/AccordKey.java | 480 +++++++++++
.../service/accord/api/AccordScheduler.java | 97 +++
.../service/accord/async/AsyncContext.java | 116 +++
.../service/accord/async/AsyncLoader.java | 240 ++++++
.../service/accord/async/AsyncOperation.java | 253 ++++++
.../service/accord/async/AsyncWriter.java | 321 +++++++
.../service/accord/db/AbstractKeyIndexed.java | 188 +++++
.../cassandra/service/accord/db/AccordData.java | 189 +++++
.../cassandra/service/accord/db/AccordQuery.java | 89 ++
.../cassandra/service/accord/db/AccordRead.java | 138 +++
.../cassandra/service/accord/db/AccordUpdate.java | 927 +++++++++++++++++++++
.../cassandra/service/accord/db/AccordWrite.java | 102 +++
.../accord/serializers/AcceptSerializers.java | 179 ++++
.../serializers/ApplyAndCheckSerializers.java | 99 +++
.../accord/serializers/ApplySerializers.java | 92 ++
.../serializers/BeginInvalidateSerializers.java | 126 +++
.../accord/serializers/CheckStatusSerializers.java | 163 ++++
.../accord/serializers/CommandSerializers.java | 272 ++++++
.../accord/serializers/CommitSerializers.java | 93 +++
.../InformOfPersistenceSerializers.java | 63 ++
.../accord/serializers/InformOfTxnSerializers.java | 82 ++
.../service/accord/serializers/KeySerializers.java | 98 +++
.../accord/serializers/NullableSerializer.java | 48 ++
.../accord/serializers/PreacceptSerializers.java | 104 +++
.../accord/serializers/ReadDataSerializers.java | 108 +++
.../accord/serializers/RecoverySerializers.java | 192 +++++
.../accord/serializers/TopologySerializers.java | 73 ++
.../accord/serializers/TxnRequestSerializer.java | 100 +++
.../accord/serializers/WaitOnCommitSerializer.java | 73 ++
.../service/accord/store/AbstractStoredField.java | 152 ++++
.../service/accord/store/StoredBoolean.java | 85 ++
.../cassandra/service/accord/store/StoredLong.java | 86 ++
.../service/accord/store/StoredNavigableMap.java | 201 +++++
.../cassandra/service/accord/store/StoredSet.java | 243 ++++++
.../service/accord/store/StoredValue.java | 128 +++
.../org/apache/cassandra/utils/ByteBufferUtil.java | 7 +
src/java/org/apache/cassandra/utils/Hex.java | 13 +-
.../org/apache/cassandra/utils/btree/BTreeSet.java | 9 +
.../cassandra/utils/concurrent/FutureCombiner.java | 1 +
.../cassandra/utils/concurrent/Semaphore.java | 1 +
test/conf/logback-simulator.xml | 10 +-
.../cassandra/distributed/impl/Instance.java | 5 +-
.../cassandra/distributed/impl/MessageImpl.java | 1 +
.../distributed/impl/UnsafeGossipHelper.java | 1 +
.../shared/VersionedApplicationState.java | 1 +
.../test/accord/AccordIntegrationTest.java | 441 ++++++++++
.../accord/AccordTopologyTest.java} | 29 +-
.../distributed/util/QueryResultUtil.java | 31 +
.../apache/cassandra/simulator/ActionSchedule.java | 12 +-
.../cassandra/simulator/ClusterSimulation.java | 3 +
.../main/org/apache/cassandra/simulator/Debug.java | 32 +-
.../cassandra/simulator/SimulationRunner.java | 22 +-
.../simulator/cluster/KeyspaceActions.java | 12 +-
.../simulator/logging/RunStartDefiner.java} | 25 +-
.../cassandra/simulator/logging/SeedDefiner.java} | 27 +-
...=> AbstractPairOfSequencesPaxosSimulation.java} | 149 +---
.../simulator/paxos/AccordClusterSimulation.java | 87 ++
.../simulator/paxos/AccordSimulationRunner.java | 78 ++
.../paxos/PairOfSequencesAccordSimulation.java | 304 +++++++
.../paxos/PairOfSequencesPaxosSimulation.java | 222 +----
.../cassandra/simulator/paxos/PaxosSimulation.java | 18 +-
.../systems/InterceptingGlobalMethods.java | 1 -
.../simulator/systems/InterceptingMonitors.java | 16 +-
.../systems/InterceptorOfGlobalMethods.java | 13 +-
.../simulator/systems/SimulatedAction.java | 3 +-
.../simulator/test/ShortAccordSimulationTest.java} | 21 +-
.../cql3/statements/DescribeStatementTest.java | 1 +
.../org/apache/cassandra/dht/KeyCollisionTest.java | 6 +
.../service/accord/AccordCommandStoreTest.java | 182 ++++
.../service/accord/AccordCommandTest.java | 186 +++++
.../service/accord/AccordStateCacheTest.java | 494 +++++++++++
.../cassandra/service/accord/AccordTestUtils.java | 257 ++++++
.../service/accord/AccordTopologyTest.java | 71 ++
.../service/accord/EndpointMappingTest.java} | 27 +-
.../service/accord/api/AccordKeyTest.java | 116 +++
.../service/accord/async/AsyncLoaderTest.java | 342 ++++++++
.../service/accord/async/AsyncOperationTest.java | 232 ++++++
.../service/accord/async/AsyncWriterTest.java | 252 ++++++
.../service/accord/db/AccordUpdateTest.java | 58 ++
.../accord/serializers/CommandSerializersTest.java | 54 ++
.../serializers/TopologySerializersTest.java} | 25 +-
.../service/accord/store/StoredMapTest.java | 203 +++++
.../service/accord/store/StoredSetTest.java | 202 +++++
.../service/accord/store/StoredValueTest.java | 85 ++
.../cassandra/utils/SerializerTestUtils.java | 74 ++
145 files changed, 15785 insertions(+), 500 deletions(-)
diff --git a/.build/cassandra-deps-template.xml b/.build/cassandra-deps-template.xml
index 513fcbc5aa..998720e7f2 100644
--- a/.build/cassandra-deps-template.xml
+++ b/.build/cassandra-deps-template.xml
@@ -129,6 +129,10 @@
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
</dependency>
+ <dependency>
+ <groupId>accord</groupId>
+ <artifactId>accord</artifactId>
+ </dependency>
<dependency>
<groupId>io.airlift</groupId>
<artifactId>airline</artifactId>
diff --git a/.build/include-accord.sh b/.build/include-accord.sh
new file mode 100755
index 0000000000..7069e22b6f
--- /dev/null
+++ b/.build/include-accord.sh
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+#
+# 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.
+#
+
+#set -o xtrace
+set -o errexit
+set -o pipefail
+set -o nounset
+
+bin="$(cd "$(dirname "$0")" > /dev/null; pwd)"
+
+accord_repo='https://github.com/bdeggleston/cassandra-accord.git'
+accord_branch='metadata-persistence'
+accord_src="$bin/cassandra-accord"
+
+checkout() {
+ cd "$accord_src"
+ git checkout "$accord_branch"
+ echo "$accord_branch" > .BRANCH
+ cd -
+}
+
+_main() {
+ # have we already cloned?
+ if [[ ! -e "$accord_src" ]] || [[ $(cat "$accord_src/.REPO" || true) != "$accord_repo" ]]; then
+ rm -rf "$accord_src" || true
+ git clone "$accord_repo" "$accord_src"
+ echo "$accord_repo" > "$accord_src/.REPO"
+ checkout
+ fi
+ if [[ $(cat "$accord_src"/.BRANCH || true) != "$accord_branch" ]]; then
+ checkout
+ fi
+ cd "$accord_src"
+ # are there changes?
+ git pull --rebase origin "$accord_branch"
+ if [[ $(git rev-parse HEAD) != $(cat .SHA || true) ]]; then
+ ./gradlew clean install -x test
+ git rev-parse HEAD > .SHA
+ fi
+}
+
+_main "$@"
diff --git a/.build/parent-pom-template.xml b/.build/parent-pom-template.xml
index 73a6431408..b57beb2d93 100644
--- a/.build/parent-pom-template.xml
+++ b/.build/parent-pom-template.xml
@@ -668,6 +668,11 @@
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
</dependency>
+ <dependency>
+ <groupId>accord</groupId>
+ <artifactId>accord</artifactId>
+ <version>1.0-SNAPSHOT</version>
+ </dependency>
<dependency>
<groupId>io.airlift</groupId>
<artifactId>airline</artifactId>
diff --git a/.gitignore b/.gitignore
index c8e79b312a..91715e4aa6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -83,3 +83,4 @@ venv/
# build-scripts will put cassandra-builds into the directory
cassandra-builds/
cassandra-dtest/
+.build/cassandra-accord/
diff --git a/CHANGES.txt b/CHANGES.txt
index 6d6c736e1e..b87425b419 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,6 @@
+accord
+ * CEP-15 (C*): Messaging and storage engine integration (CASSANDRA-17103)
+
4.2
* Prevent a user from manually removing ephemeral snapshots (CASSANDRA-17757)
* Remove dependency on Maven Ant Tasks (CASSANDRA-17750)
diff --git a/build.xml b/build.xml
index a18fb6b72a..8628de5e8f 100644
--- a/build.xml
+++ b/build.xml
@@ -269,6 +269,11 @@
</fileset>
</path>
+ <target name="_build-accord">
+ <echo unless:set="env.ACCORD_LOCAL_DEV">building Accord, set ACCORD_LOCAL_DEV environment variable to skip</echo>
+ <exec unless:set="env.ACCORD_LOCAL_DEV" executable=".build/include-accord.sh" dir="${basedir}" logError="true" failonerror="true" failifexecutionfails="true" />
+ <echo if:set="env.ACCORD_LOCAL_DEV">ACCORD_LOCAL_DEV environment variable is set, skipping pull/install</echo>
+ </target>
<macrodef name="create-javadoc">
<attribute name="destdir"/>
<element name="filesets"/>
@@ -340,7 +345,7 @@
<!--
Setup the output directories.
-->
- <target name="init" depends="validate-build-conf">
+ <target name="init" depends="validate-build-conf,_build-accord">
<fail unless="is.source.artifact"
message="Not a source artifact, stopping here." />
<mkdir dir="${build.classes.main}"/>
diff --git a/simulator.sh b/simulator.sh
new file mode 100755
index 0000000000..516405e974
--- /dev/null
+++ b/simulator.sh
@@ -0,0 +1,88 @@
+#!/bin/bash
+#
+# 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.
+#
+
+#ant jar simulator-jars
+
+DIR=`pwd`
+JVM_OPTS="$JVM_OPTS -Dcassandra.config=file://$DIR/test/conf/cassandra.yaml"
+JVM_OPTS="$JVM_OPTS -Dlogback.configurationFile=file://$DIR/test/conf/logback-simulator.xml"
+JVM_OPTS="$JVM_OPTS -Dcassandra.logdir=$DIR/build/test/logs"
+#JVM_OPTS="$JVM_OPTS -Djava.library.path=$DIR/lib/sigar-bin"
+JVM_OPTS="$JVM_OPTS -Dlegacy-sstable-root=$DIR/test/data/legacy-sstables"
+JVM_OPTS="$JVM_OPTS -Dinvalid-legacy-sstable-root=$DIR/test/data/invalid-legacy-sstables"
+JVM_OPTS="$JVM_OPTS -Dcassandra.ring_delay_ms=1000"
+JVM_OPTS="$JVM_OPTS -Dcassandra.skip_sync=true"
+JVM_OPTS="$JVM_OPTS -ea"
+JVM_OPTS="$JVM_OPTS -XX:MaxMetaspaceSize=1G"
+JVM_OPTS="$JVM_OPTS -XX:SoftRefLRUPolicyMSPerMB=0"
+JVM_OPTS="$JVM_OPTS -Dcassandra.strict.runtime.checks=true"
+JVM_OPTS="$JVM_OPTS -javaagent:$DIR/build/test/lib/jars/simulator-asm.jar"
+JVM_OPTS="$JVM_OPTS -Xbootclasspath/a:$DIR/build/test/lib/jars/simulator-bootstrap.jar"
+JVM_OPTS="$JVM_OPTS -XX:ActiveProcessorCount=4"
+JVM_OPTS="$JVM_OPTS -XX:-TieredCompilation"
+JVM_OPTS="$JVM_OPTS -XX:Tier4CompileThreshold=1000"
+JVM_OPTS="$JVM_OPTS -XX:ReservedCodeCacheSize=256M"
+JVM_OPTS="$JVM_OPTS -Xmx8G"
+JVM_OPTS="$JVM_OPTS -Dcassandra.test.simulator.determinismcheck=strict"
+JVM_OPTS="$JVM_OPTS -Dcassandra.debugrefcount=false"
+JVM_OPTS="$JVM_OPTS -Dcassandra.skip_sync=true"
+JVM_OPTS="$JVM_OPTS -Dcassandra.tolerate_sstable_size=true"
+JVM_OPTS="$JVM_OPTS -Dcassandra.test.simulator.debug=true"
+JVM_OPTS="$JVM_OPTS -Dcassandra.test.simulator.determinismcheck=strict"
+echo $JVM_OPTS
+
+CLASSPATH="$DIR"/build/test/classes
+for dir in "$DIR"/build/classes/*; do
+ CLASSPATH="$CLASSPATH:$dir"
+done
+
+for jar in "$DIR"/lib/*.jar; do
+ CLASSPATH="$CLASSPATH:$jar"
+done
+for jar in "$DIR"/build/*.jar; do
+ if [[ $jar != *"logback-classic"* ]]; then
+ CLASSPATH="$CLASSPATH:$jar"
+ fi
+done
+for jar in "$DIR"/build/lib/jars/*.jar; do
+ if [[ $jar != *"logback-classic"* ]]; then
+ CLASSPATH="$CLASSPATH:$jar"
+ fi
+done
+for jar in "$DIR"/build/test/lib/jars/*.jar; do
+ if [[ $jar != *"logback-classic"* ]]; then
+ CLASSPATH="$CLASSPATH:$jar"
+ fi
+done
+
+CLASS="org.apache.cassandra.simulator.paxos.AccordSimulationRunner"
+OPTS="run -n 3..6 -t 1000 --cluster-action-limit -1 -c 2 -s 30"
+
+echo "java -cp <...> $CLASS $OPTS $@"
+
+while true
+do
+ echo ""
+ java -cp $CLASSPATH $JVM_OPTS $CLASS $OPTS $@
+ status=$?
+ if [ $status -ne 0 ] ; then
+ exit $status
+ fi
+
+done
diff --git a/src/java/org/apache/cassandra/concurrent/SingleThreadExecutorPlus.java b/src/java/org/apache/cassandra/concurrent/SingleThreadExecutorPlus.java
index eb2827774a..553855ad7b 100644
--- a/src/java/org/apache/cassandra/concurrent/SingleThreadExecutorPlus.java
+++ b/src/java/org/apache/cassandra/concurrent/SingleThreadExecutorPlus.java
@@ -25,6 +25,7 @@ public class SingleThreadExecutorPlus extends ThreadPoolExecutorPlus implements
{
public static class AtLeastOnce extends AtomicBoolean implements AtLeastOnceTrigger, Runnable
{
+ private static final long serialVersionUID = 0; // for simulator support
protected final SequentialExecutorPlus executor;
protected final Runnable run;
diff --git a/src/java/org/apache/cassandra/concurrent/Stage.java b/src/java/org/apache/cassandra/concurrent/Stage.java
index ac609aa774..4a7552ab24 100644
--- a/src/java/org/apache/cassandra/concurrent/Stage.java
+++ b/src/java/org/apache/cassandra/concurrent/Stage.java
@@ -46,6 +46,7 @@ public enum Stage
MUTATION (true, "MutationStage", "request", DatabaseDescriptor::getConcurrentWriters, DatabaseDescriptor::setConcurrentWriters, Stage::multiThreadedLowSignalStage),
COUNTER_MUTATION (true, "CounterMutationStage", "request", DatabaseDescriptor::getConcurrentCounterWriters, DatabaseDescriptor::setConcurrentCounterWriters, Stage::multiThreadedLowSignalStage),
VIEW_MUTATION (true, "ViewMutationStage", "request", DatabaseDescriptor::getConcurrentViewWriters, DatabaseDescriptor::setConcurrentViewWriters, Stage::multiThreadedLowSignalStage),
+ ACCORD (true, "AccordStage", "request", DatabaseDescriptor::getConcurrentAccordOps, DatabaseDescriptor::setConcurrentAccordOps, Stage::multiThreadedLowSignalStage),
GOSSIP (true, "GossipStage", "internal", () -> 1, null, Stage::singleThreadedStage),
REQUEST_RESPONSE (false, "RequestResponseStage", "request", FBUtilities::getAvailableProcessors, null, Stage::multiThreadedLowSignalStage),
ANTI_ENTROPY (false, "AntiEntropyStage", "internal", () -> 1, null, Stage::singleThreadedStage),
@@ -56,7 +57,6 @@ public enum Stage
IMMEDIATE (false, "ImmediateStage", "internal", () -> 0, null, Stage::immediateExecutor),
PAXOS_REPAIR (false, "PaxosRepairStage", "internal", FBUtilities::getAvailableProcessors, null, Stage::multiThreadedStage),
;
-
public final String jmxName;
private final Supplier<ExecutorPlus> executorSupplier;
private volatile ExecutorPlus executor;
diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java
index 0281794759..71ac5937a1 100644
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@ -164,6 +164,7 @@ public class Config
public int concurrent_reads = 32;
public int concurrent_writes = 32;
+ public int concurrent_accord_operations = 32;
public int concurrent_counter_writes = 32;
public int concurrent_materialized_view_writes = 32;
public int available_processors = -1;
diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index 42b5e27fba..fad05e57ed 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -497,6 +497,9 @@ public class DatabaseDescriptor
if (conf.concurrent_counter_writes < 2)
throw new ConfigurationException("concurrent_counter_writes must be at least 2, but was " + conf.concurrent_counter_writes, false);
+ if (conf.concurrent_accord_operations < 1)
+ throw new ConfigurationException("concurrent_accord_operations must be at least 1, but was " + conf.concurrent_accord_operations, false);
+
if (conf.concurrent_replicates != null)
logger.warn("concurrent_replicates has been deprecated and should be removed from cassandra.yaml");
@@ -1976,6 +1979,20 @@ public class DatabaseDescriptor
conf.concurrent_materialized_view_writes = concurrent_materialized_view_writes;
}
+ public static int getConcurrentAccordOps()
+ {
+ return conf.concurrent_accord_operations;
+ }
+
+ public static void setConcurrentAccordOps(int concurrent_operations)
+ {
+ if (concurrent_operations < 0)
+ {
+ throw new IllegalArgumentException("Concurrent accord operations must be non-negative");
+ }
+ conf.concurrent_accord_operations = concurrent_operations;
+ }
+
public static int getFlushWriters()
{
return conf.memtable_flush_writers;
diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
index ab36ec971b..8f659e6385 100644
--- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
@@ -719,7 +719,7 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
*
* @return list of the mutations
*/
- private List<? extends IMutation> getMutations(ClientState state,
+ public List<? extends IMutation> getMutations(ClientState state,
QueryOptions options,
boolean local,
long timestamp,
diff --git a/src/java/org/apache/cassandra/db/MutableDeletionInfo.java b/src/java/org/apache/cassandra/db/MutableDeletionInfo.java
index 9cad53e4ea..a695209b72 100644
--- a/src/java/org/apache/cassandra/db/MutableDeletionInfo.java
+++ b/src/java/org/apache/cassandra/db/MutableDeletionInfo.java
@@ -230,6 +230,16 @@ public class MutableDeletionInfo implements DeletionInfo
return this;
}
+ public DeletionInfo updateAllTimestampAndLocalDeletionTime(long timestamp, int localDeletionTime)
+ {
+ if (partitionDeletion.markedForDeleteAt() != Long.MIN_VALUE)
+ partitionDeletion = new DeletionTime(timestamp, localDeletionTime);
+
+ if (ranges != null)
+ ranges.updateAllTimestampAndLocalDeletionTime(timestamp, localDeletionTime);
+ return this;
+ }
+
@Override
public boolean equals(Object o)
{
diff --git a/src/java/org/apache/cassandra/db/RangeTombstoneList.java b/src/java/org/apache/cassandra/db/RangeTombstoneList.java
index c8992a222b..e701fec280 100644
--- a/src/java/org/apache/cassandra/db/RangeTombstoneList.java
+++ b/src/java/org/apache/cassandra/db/RangeTombstoneList.java
@@ -320,6 +320,15 @@ public class RangeTombstoneList implements Iterable<RangeTombstone>, IMeasurable
markedAts[i] = timestamp;
}
+ public void updateAllTimestampAndLocalDeletionTime(long timestamp, int localDeletionTime)
+ {
+ for (int i = 0; i < size; i++)
+ {
+ markedAts[i] = timestamp;
+ delTimes[i] = localDeletionTime;
+ }
+ }
+
private RangeTombstone rangeTombstone(int idx)
{
return new RangeTombstone(Slice.make(starts[idx], ends[idx]), new DeletionTime(markedAts[idx], delTimes[idx]));
diff --git a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java
index 64136b6099..96af39708a 100644
--- a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java
+++ b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java
@@ -48,8 +48,10 @@ import org.apache.cassandra.metrics.TableMetrics;
import org.apache.cassandra.net.Verb;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.IndexMetadata;
+import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.*;
+import org.apache.cassandra.service.accord.api.AccordKey;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.btree.BTreeSet;
@@ -391,6 +393,22 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar
isTrackingWarnings());
}
+ public SinglePartitionReadCommand withNowInSec(int nowInSec)
+ {
+ return new SinglePartitionReadCommand(isDigestQuery(),
+ digestVersion(),
+ acceptsTransient(),
+ metadata(),
+ nowInSec,
+ columnFilter(),
+ rowFilter(),
+ limits(),
+ partitionKey(),
+ clusteringIndexFilter(),
+ indexMetadata(),
+ isTrackingWarnings());
+ }
+
@Override
public DecoratedKey partitionKey()
{
diff --git a/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java b/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java
index ddcaaed812..e1792d17f8 100644
--- a/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java
@@ -18,6 +18,7 @@
package org.apache.cassandra.db.filter;
import java.io.IOException;
+import java.util.Objects;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;
diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java
index 18dc471ef3..9d1090df6b 100644
--- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java
@@ -204,20 +204,6 @@ public class ClusteringIndexNamesFilter extends AbstractClusteringIndexFilter
return sb.toString();
}
- public boolean equals(Object o)
- {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ClusteringIndexNamesFilter that = (ClusteringIndexNamesFilter) o;
- return Objects.equals(clusterings, that.clusterings) &&
- Objects.equals(reversed, that.reversed);
- }
-
- public int hashCode()
- {
- return Objects.hash(clusterings, reversed);
- }
-
public Kind kind()
{
return Kind.NAMES;
diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java
index b96888dfbe..bcf787fee7 100644
--- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.db.filter;
import java.io.IOException;
import java.util.List;
import java.nio.ByteBuffer;
+import java.util.Objects;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.db.*;
diff --git a/src/java/org/apache/cassandra/db/partitions/Partition.java b/src/java/org/apache/cassandra/db/partitions/Partition.java
index 8888104d95..1daf20cd00 100644
--- a/src/java/org/apache/cassandra/db/partitions/Partition.java
+++ b/src/java/org/apache/cassandra/db/partitions/Partition.java
@@ -21,10 +21,12 @@ import java.util.NavigableSet;
import javax.annotation.Nullable;
+import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.rows.*;
import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.service.accord.api.AccordKey;
/**
* In-memory representation of a Partition.
@@ -37,6 +39,7 @@ import org.apache.cassandra.db.filter.ColumnFilter;
public interface Partition
{
public TableMetadata metadata();
+
public DecoratedKey partitionKey();
public DeletionTime partitionLevelDeletion();
diff --git a/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java b/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java
index 6019d56fb8..f6fc527ff5 100644
--- a/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java
+++ b/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java
@@ -795,6 +795,15 @@ public class PartitionUpdate extends AbstractBTreePartition
this(metadata, key, columns, initialRowCapacity, canHaveShadowedData, Rows.EMPTY_STATIC_ROW, MutableDeletionInfo.live(), BTree.empty());
}
+ public Builder(TableMetadata metadata,
+ DecoratedKey key,
+ RegularAndStaticColumns columns,
+ Row staticRow,
+ int initialRowCapacity)
+ {
+ this(metadata, key, columns, initialRowCapacity, true, staticRow, MutableDeletionInfo.live(), BTree.empty());
+ }
+
private Builder(TableMetadata metadata,
DecoratedKey key,
RegularAndStaticColumns columns,
@@ -955,6 +964,14 @@ public class PartitionUpdate extends AbstractBTreePartition
return this;
}
+ public Builder updateAllTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime)
+ {
+ deletionInfo.updateAllTimestampAndLocalDeletionTime(newTimestamp - 1, newLocalDeletionTime);
+ tree = BTree.<Row, Row>transformAndFilter(tree, (x) -> x.updateAllTimestampAndLocalDeletionTime(newTimestamp, newLocalDeletionTime));
+ staticRow = this.staticRow.updateAllTimestampAndLocalDeletionTime(newTimestamp, newLocalDeletionTime);
+ return this;
+ }
+
@Override
public String toString()
{
diff --git a/src/java/org/apache/cassandra/db/rows/AbstractCell.java b/src/java/org/apache/cassandra/db/rows/AbstractCell.java
index b9b5d6044c..67263782e4 100644
--- a/src/java/org/apache/cassandra/db/rows/AbstractCell.java
+++ b/src/java/org/apache/cassandra/db/rows/AbstractCell.java
@@ -117,6 +117,13 @@ public abstract class AbstractCell<V> extends Cell<V>
return new BufferCell(column, isTombstone() ? newTimestamp - 1 : newTimestamp, ttl(), localDeletionTime(), buffer(), path());
}
+ @Override
+ public ColumnData updateAllTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime)
+ {
+ int localDeletionTime = localDeletionTime() != NO_DELETION_TIME ? newLocalDeletionTime : NO_DELETION_TIME;
+ return new BufferCell(column, isTombstone() ? newTimestamp - 1 : newTimestamp, ttl(), localDeletionTime, buffer(), path());
+ }
+
public int dataSize()
{
CellPath path = path();
diff --git a/src/java/org/apache/cassandra/db/rows/BTreeRow.java b/src/java/org/apache/cassandra/db/rows/BTreeRow.java
index 125932be0f..2cdbc65ae1 100644
--- a/src/java/org/apache/cassandra/db/rows/BTreeRow.java
+++ b/src/java/org/apache/cassandra/db/rows/BTreeRow.java
@@ -442,6 +442,18 @@ public class BTreeRow extends AbstractRow
return transformAndFilter(newInfo, newDeletion, (cd) -> cd.updateAllTimestamp(newTimestamp));
}
+ @Override
+ public Row updateAllTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime)
+ {
+ LivenessInfo newInfo = primaryKeyLivenessInfo.isEmpty() ? primaryKeyLivenessInfo : primaryKeyLivenessInfo.withUpdatedTimestampAndLocalDeletionTime(newTimestamp, newLocalDeletionTime);
+ // If the deletion is shadowable and the row has a timestamp, we'll forced the deletion timestamp to be less than the row one, so we
+ // should get rid of said deletion.
+ Deletion newDeletion = deletion.isLive() || (deletion.isShadowable() && !primaryKeyLivenessInfo.isEmpty())
+ ? Deletion.LIVE
+ : new Deletion(new DeletionTime(newTimestamp - 1, newLocalDeletionTime), deletion.isShadowable());
+ return transformAndFilter(newInfo, newDeletion, (cd) -> cd.updateAllTimestampAndLocalDeletionTime(newTimestamp, newLocalDeletionTime));
+ }
+
public Row withRowDeletion(DeletionTime newDeletion)
{
// Note that:
diff --git a/src/java/org/apache/cassandra/db/rows/ColumnData.java b/src/java/org/apache/cassandra/db/rows/ColumnData.java
index ae2770669c..ade18c64a2 100644
--- a/src/java/org/apache/cassandra/db/rows/ColumnData.java
+++ b/src/java/org/apache/cassandra/db/rows/ColumnData.java
@@ -265,6 +265,7 @@ public abstract class ColumnData implements IMeasurableMemory
* This exists for the Paxos path, see {@link PartitionUpdate#updateAllTimestamp} for additional details.
*/
public abstract ColumnData updateAllTimestamp(long newTimestamp);
+ public abstract ColumnData updateAllTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime);
public abstract ColumnData markCounterLocalToBeCleared();
diff --git a/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java b/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java
index bfd7f125f7..ba019cac42 100644
--- a/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java
+++ b/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java
@@ -261,6 +261,13 @@ public class ComplexColumnData extends ColumnData implements Iterable<Cell<?>>
return transformAndFilter(newDeletion, (cell) -> (Cell<?>) cell.updateAllTimestamp(newTimestamp));
}
+ @Override
+ public ColumnData updateAllTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime)
+ {
+ DeletionTime newDeletion = complexDeletion.isLive() ? complexDeletion : new DeletionTime(newTimestamp - 1, newLocalDeletionTime);
+ return transformAndFilter(newDeletion, (cell) -> (Cell<?>) cell.updateAllTimestampAndLocalDeletionTime(newTimestamp, newLocalDeletionTime));
+ }
+
public long maxTimestamp()
{
long timestamp = complexDeletion.markedForDeleteAt();
diff --git a/src/java/org/apache/cassandra/db/rows/Row.java b/src/java/org/apache/cassandra/db/rows/Row.java
index 9e8276a8d7..2ff4ae90a8 100644
--- a/src/java/org/apache/cassandra/db/rows/Row.java
+++ b/src/java/org/apache/cassandra/db/rows/Row.java
@@ -299,6 +299,8 @@ public interface Row extends Unfiltered, Iterable<ColumnData>, IMeasurableMemory
*/
public Row updateAllTimestamp(long newTimestamp);
+ public Row updateAllTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime);
+
/**
* Returns a copy of this row with the new deletion as row deletion if it is more recent
* than the current row deletion.
diff --git a/src/java/org/apache/cassandra/dht/ByteOrderedPartitioner.java b/src/java/org/apache/cassandra/dht/ByteOrderedPartitioner.java
index 2b0e2a2861..2c3c9aab36 100644
--- a/src/java/org/apache/cassandra/dht/ByteOrderedPartitioner.java
+++ b/src/java/org/apache/cassandra/dht/ByteOrderedPartitioner.java
@@ -128,6 +128,12 @@ public class ByteOrderedPartitioner implements IPartitioner
return token;
}
+ @Override
+ public int tokenHash()
+ {
+ return hashCode();
+ }
+
@Override
public double size(Token next)
{
diff --git a/src/java/org/apache/cassandra/dht/LocalPartitioner.java b/src/java/org/apache/cassandra/dht/LocalPartitioner.java
index 127c5b7ded..ad8461962d 100644
--- a/src/java/org/apache/cassandra/dht/LocalPartitioner.java
+++ b/src/java/org/apache/cassandra/dht/LocalPartitioner.java
@@ -172,6 +172,12 @@ public class LocalPartitioner implements IPartitioner
return prime + token.hashCode();
}
+ @Override
+ public int tokenHash()
+ {
+ return hashCode();
+ }
+
@Override
public boolean equals(Object obj)
{
diff --git a/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java b/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java
index 015610fb53..8a8ec55d70 100644
--- a/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java
+++ b/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java
@@ -203,6 +203,12 @@ public class Murmur3Partitioner implements IPartitioner
return token;
}
+ @Override
+ public int tokenHash()
+ {
+ return Long.hashCode(token);
+ }
+
@Override
public double size(Token next)
{
diff --git a/src/java/org/apache/cassandra/dht/OrderPreservingPartitioner.java b/src/java/org/apache/cassandra/dht/OrderPreservingPartitioner.java
index 2d4def95d0..18cd94c388 100644
--- a/src/java/org/apache/cassandra/dht/OrderPreservingPartitioner.java
+++ b/src/java/org/apache/cassandra/dht/OrderPreservingPartitioner.java
@@ -208,6 +208,12 @@ public class OrderPreservingPartitioner implements IPartitioner
{
return ByteSource.of(token, version);
}
+
+ @Override
+ public int tokenHash()
+ {
+ return token.hashCode();
+ }
}
public StringToken getToken(ByteBuffer key)
diff --git a/src/java/org/apache/cassandra/dht/RandomPartitioner.java b/src/java/org/apache/cassandra/dht/RandomPartitioner.java
index d02cfd58ad..a27d457b09 100644
--- a/src/java/org/apache/cassandra/dht/RandomPartitioner.java
+++ b/src/java/org/apache/cassandra/dht/RandomPartitioner.java
@@ -276,6 +276,12 @@ public class RandomPartitioner implements IPartitioner
return new BigIntegerToken(token.add(BigInteger.ONE));
}
+ @Override
+ public int tokenHash()
+ {
+ return token.hashCode();
+ }
+
public double size(Token next)
{
BigIntegerToken n = (BigIntegerToken) next;
diff --git a/src/java/org/apache/cassandra/dht/Token.java b/src/java/org/apache/cassandra/dht/Token.java
index 3543dabc0e..be7bbbb113 100644
--- a/src/java/org/apache/cassandra/dht/Token.java
+++ b/src/java/org/apache/cassandra/dht/Token.java
@@ -123,6 +123,7 @@ public abstract class Token implements RingPosition<Token>, Serializable
abstract public IPartitioner getPartitioner();
abstract public long getHeapSize();
abstract public Object getTokenValue();
+ abstract public int tokenHash();
/**
* Produce a weakly prefix-free byte-comparable representation of the token, i.e. such a sequence of bytes that any
diff --git a/src/java/org/apache/cassandra/metrics/Sampler.java b/src/java/org/apache/cassandra/metrics/Sampler.java
index de7f0b2e1a..b3492a44a1 100644
--- a/src/java/org/apache/cassandra/metrics/Sampler.java
+++ b/src/java/org/apache/cassandra/metrics/Sampler.java
@@ -153,6 +153,7 @@ public abstract class Sampler<T>
*/
public static class Sample<S> implements Serializable
{
+ private static final long serialVersionUID = 0; // for simulator support
public final S value;
public final long count;
public final long error;
diff --git a/src/java/org/apache/cassandra/net/ForwardingInfo.java b/src/java/org/apache/cassandra/net/ForwardingInfo.java
index 76e2a75372..4de3337605 100644
--- a/src/java/org/apache/cassandra/net/ForwardingInfo.java
+++ b/src/java/org/apache/cassandra/net/ForwardingInfo.java
@@ -44,6 +44,7 @@ import static org.apache.cassandra.utils.vint.VIntCoding.computeUnsignedVIntSize
*/
public final class ForwardingInfo implements Serializable
{
+ private static final long serialVersionUID = 0; // for simulator support
final List<InetAddressAndPort> targets;
final long[] messageIds;
diff --git a/src/java/org/apache/cassandra/net/Message.java b/src/java/org/apache/cassandra/net/Message.java
index 472c5aacde..1ed5f03df1 100644
--- a/src/java/org/apache/cassandra/net/Message.java
+++ b/src/java/org/apache/cassandra/net/Message.java
@@ -33,6 +33,7 @@ import com.google.common.primitives.Ints;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import accord.messages.ReplyContext;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.exceptions.RequestFailureReason;
import org.apache.cassandra.io.IVersionedAsymmetricSerializer;
@@ -68,7 +69,7 @@ import static org.apache.cassandra.utils.vint.VIntCoding.skipUnsignedVInt;
*
* @param <T> The type of the message payload.
*/
-public class Message<T>
+public class Message<T> implements ReplyContext
{
private static final Logger logger = LoggerFactory.getLogger(Message.class);
private static final NoSpamLogger noSpam1m = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES);
diff --git a/src/java/org/apache/cassandra/net/ResponseVerbHandler.java b/src/java/org/apache/cassandra/net/ResponseVerbHandler.java
index 1cee468cd3..3518888591 100644
--- a/src/java/org/apache/cassandra/net/ResponseVerbHandler.java
+++ b/src/java/org/apache/cassandra/net/ResponseVerbHandler.java
@@ -26,7 +26,7 @@ import org.apache.cassandra.tracing.Tracing;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime;
-class ResponseVerbHandler implements IVerbHandler
+public class ResponseVerbHandler implements IVerbHandler
{
public static final ResponseVerbHandler instance = new ResponseVerbHandler();
diff --git a/src/java/org/apache/cassandra/net/Verb.java b/src/java/org/apache/cassandra/net/Verb.java
index d50a187fda..f8ec811e7a 100644
--- a/src/java/org/apache/cassandra/net/Verb.java
+++ b/src/java/org/apache/cassandra/net/Verb.java
@@ -21,6 +21,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.ToLongFunction;
@@ -75,6 +76,11 @@ import org.apache.cassandra.schema.SchemaMutationsSerializer;
import org.apache.cassandra.schema.SchemaPullVerbHandler;
import org.apache.cassandra.schema.SchemaPushVerbHandler;
import org.apache.cassandra.schema.SchemaVersionVerbHandler;
+import org.apache.cassandra.service.accord.serializers.ApplyAndCheckSerializers;
+import org.apache.cassandra.service.accord.serializers.BeginInvalidateSerializers;
+import org.apache.cassandra.service.accord.serializers.CheckStatusSerializers;
+import org.apache.cassandra.service.accord.serializers.InformOfPersistenceSerializers;
+import org.apache.cassandra.service.accord.serializers.InformOfTxnSerializers;
import org.apache.cassandra.service.paxos.PaxosCommit;
import org.apache.cassandra.service.paxos.PaxosCommitAndPrepare;
import org.apache.cassandra.service.paxos.PaxosPrepare;
@@ -87,6 +93,14 @@ import org.apache.cassandra.service.paxos.cleanup.PaxosCleanupResponse;
import org.apache.cassandra.service.paxos.cleanup.PaxosCleanupComplete;
import org.apache.cassandra.service.paxos.cleanup.PaxosStartPrepareCleanup;
import org.apache.cassandra.service.paxos.cleanup.PaxosFinishPrepareCleanup;
+import org.apache.cassandra.service.accord.AccordService;
+import org.apache.cassandra.service.accord.serializers.AcceptSerializers;
+import org.apache.cassandra.service.accord.serializers.ApplySerializers;
+import org.apache.cassandra.service.accord.serializers.CommitSerializers;
+import org.apache.cassandra.service.accord.serializers.PreacceptSerializers;
+import org.apache.cassandra.service.accord.serializers.ReadDataSerializers;
+import org.apache.cassandra.service.accord.serializers.RecoverySerializers;
+import org.apache.cassandra.service.accord.serializers.WaitOnCommitSerializer;
import org.apache.cassandra.utils.BooleanSerializer;
import org.apache.cassandra.service.EchoVerbHandler;
import org.apache.cassandra.service.SnapshotVerbHandler;
@@ -104,39 +118,41 @@ import static org.apache.cassandra.concurrent.Stage.*;
import static org.apache.cassandra.net.VerbTimeouts.*;
import static org.apache.cassandra.net.Verb.Kind.*;
import static org.apache.cassandra.net.Verb.Priority.*;
+import static org.apache.cassandra.net.ResponseHandlerSupplier.RESPONSE_HANDLER;
/**
* Note that priorities except P0 are presently unused. P0 corresponds to urgent, i.e. what used to be the "Gossip" connection.
*/
+@SuppressWarnings("Convert2MethodRef") // we must defer all initialisation, which includes e.g. taking a method reference to a static object/singleton, which this inspection does not disambiguate
public enum Verb
{
- MUTATION_RSP (60, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
+ MUTATION_RSP (60, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, RESPONSE_HANDLER ),
MUTATION_REQ (0, P3, writeTimeout, MUTATION, () -> Mutation.serializer, () -> MutationVerbHandler.instance, MUTATION_RSP ),
- HINT_RSP (61, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
+ HINT_RSP (61, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, RESPONSE_HANDLER ),
HINT_REQ (1, P4, writeTimeout, MUTATION, () -> HintMessage.serializer, () -> HintVerbHandler.instance, HINT_RSP ),
- READ_REPAIR_RSP (62, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
+ READ_REPAIR_RSP (62, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, RESPONSE_HANDLER ),
READ_REPAIR_REQ (2, P1, writeTimeout, MUTATION, () -> Mutation.serializer, () -> ReadRepairVerbHandler.instance, READ_REPAIR_RSP ),
- BATCH_STORE_RSP (65, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
+ BATCH_STORE_RSP (65, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, RESPONSE_HANDLER ),
BATCH_STORE_REQ (5, P3, writeTimeout, MUTATION, () -> Batch.serializer, () -> BatchStoreVerbHandler.instance, BATCH_STORE_RSP ),
- BATCH_REMOVE_RSP (66, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
+ BATCH_REMOVE_RSP (66, P1, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, RESPONSE_HANDLER ),
BATCH_REMOVE_REQ (6, P3, writeTimeout, MUTATION, () -> TimeUUID.Serializer.instance, () -> BatchRemoveVerbHandler.instance, BATCH_REMOVE_RSP ),
- PAXOS_PREPARE_RSP (93, P2, writeTimeout, REQUEST_RESPONSE, () -> PrepareResponse.serializer, () -> ResponseVerbHandler.instance ),
+ PAXOS_PREPARE_RSP (93, P2, writeTimeout, REQUEST_RESPONSE, () -> PrepareResponse.serializer, RESPONSE_HANDLER ),
PAXOS_PREPARE_REQ (33, P2, writeTimeout, MUTATION, () -> Commit.serializer, () -> PrepareVerbHandler.instance, PAXOS_PREPARE_RSP ),
- PAXOS_PROPOSE_RSP (94, P2, writeTimeout, REQUEST_RESPONSE, () -> BooleanSerializer.serializer, () -> ResponseVerbHandler.instance ),
+ PAXOS_PROPOSE_RSP (94, P2, writeTimeout, REQUEST_RESPONSE, () -> BooleanSerializer.serializer, RESPONSE_HANDLER ),
PAXOS_PROPOSE_REQ (34, P2, writeTimeout, MUTATION, () -> Commit.serializer, () -> ProposeVerbHandler.instance, PAXOS_PROPOSE_RSP ),
- PAXOS_COMMIT_RSP (95, P2, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
+ PAXOS_COMMIT_RSP (95, P2, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, RESPONSE_HANDLER ),
PAXOS_COMMIT_REQ (35, P2, writeTimeout, MUTATION, () -> Agreed.serializer, () -> PaxosCommit.requestHandler, PAXOS_COMMIT_RSP ),
- TRUNCATE_RSP (79, P0, truncateTimeout, REQUEST_RESPONSE, () -> TruncateResponse.serializer, () -> ResponseVerbHandler.instance ),
+ TRUNCATE_RSP (79, P0, truncateTimeout, REQUEST_RESPONSE, () -> TruncateResponse.serializer, RESPONSE_HANDLER ),
TRUNCATE_REQ (19, P0, truncateTimeout, MUTATION, () -> TruncateRequest.serializer, () -> TruncateVerbHandler.instance, TRUNCATE_RSP ),
- COUNTER_MUTATION_RSP (84, P1, counterTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
+ COUNTER_MUTATION_RSP (84, P1, counterTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, RESPONSE_HANDLER ),
COUNTER_MUTATION_REQ (24, P2, counterTimeout, COUNTER_MUTATION, () -> CounterMutation.serializer, () -> CounterMutationVerbHandler.instance, COUNTER_MUTATION_RSP),
- READ_RSP (63, P2, readTimeout, REQUEST_RESPONSE, () -> ReadResponse.serializer, () -> ResponseVerbHandler.instance ),
+ READ_RSP (63, P2, readTimeout, REQUEST_RESPONSE, () -> ReadResponse.serializer, RESPONSE_HANDLER ),
READ_REQ (3, P3, readTimeout, READ, () -> ReadCommand.serializer, () -> ReadCommandVerbHandler.instance, READ_RSP ),
- RANGE_RSP (69, P2, rangeTimeout, REQUEST_RESPONSE, () -> ReadResponse.serializer, () -> ResponseVerbHandler.instance ),
+ RANGE_RSP (69, P2, rangeTimeout, REQUEST_RESPONSE, () -> ReadResponse.serializer, RESPONSE_HANDLER ),
RANGE_REQ (9, P3, rangeTimeout, READ, () -> ReadCommand.serializer, () -> ReadCommandVerbHandler.instance, RANGE_RSP ),
GOSSIP_DIGEST_SYN (14, P0, longTimeout, GOSSIP, () -> GossipDigestSyn.serializer, () -> GossipDigestSynVerbHandler.instance ),
@@ -144,21 +160,21 @@ public enum Verb
GOSSIP_DIGEST_ACK2 (16, P0, longTimeout, GOSSIP, () -> GossipDigestAck2.serializer, () -> GossipDigestAck2VerbHandler.instance ),
GOSSIP_SHUTDOWN (29, P0, rpcTimeout, GOSSIP, () -> NoPayload.serializer, () -> GossipShutdownVerbHandler.instance ),
- ECHO_RSP (91, P0, rpcTimeout, GOSSIP, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
+ ECHO_RSP (91, P0, rpcTimeout, GOSSIP, () -> NoPayload.serializer, RESPONSE_HANDLER ),
ECHO_REQ (31, P0, rpcTimeout, GOSSIP, () -> NoPayload.serializer, () -> EchoVerbHandler.instance, ECHO_RSP ),
- PING_RSP (97, P1, pingTimeout, GOSSIP, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
+ PING_RSP (97, P1, pingTimeout, GOSSIP, () -> NoPayload.serializer, RESPONSE_HANDLER ),
PING_REQ (37, P1, pingTimeout, GOSSIP, () -> PingRequest.serializer, () -> PingVerbHandler.instance, PING_RSP ),
// P1 because messages can be arbitrarily large or aren't crucial
- SCHEMA_PUSH_RSP (98, P1, rpcTimeout, MIGRATION, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
+ SCHEMA_PUSH_RSP (98, P1, rpcTimeout, MIGRATION, () -> NoPayload.serializer, RESPONSE_HANDLER ),
SCHEMA_PUSH_REQ (18, P1, rpcTimeout, MIGRATION, () -> SchemaMutationsSerializer.instance, () -> SchemaPushVerbHandler.instance, SCHEMA_PUSH_RSP ),
- SCHEMA_PULL_RSP (88, P1, rpcTimeout, MIGRATION, () -> SchemaMutationsSerializer.instance, () -> ResponseVerbHandler.instance ),
+ SCHEMA_PULL_RSP (88, P1, rpcTimeout, MIGRATION, () -> SchemaMutationsSerializer.instance, RESPONSE_HANDLER ),
SCHEMA_PULL_REQ (28, P1, rpcTimeout, MIGRATION, () -> NoPayload.serializer, () -> SchemaPullVerbHandler.instance, SCHEMA_PULL_RSP ),
- SCHEMA_VERSION_RSP (80, P1, rpcTimeout, MIGRATION, () -> UUIDSerializer.serializer, () -> ResponseVerbHandler.instance ),
+ SCHEMA_VERSION_RSP (80, P1, rpcTimeout, MIGRATION, () -> UUIDSerializer.serializer, RESPONSE_HANDLER ),
SCHEMA_VERSION_REQ (20, P1, rpcTimeout, MIGRATION, () -> NoPayload.serializer, () -> SchemaVersionVerbHandler.instance, SCHEMA_VERSION_RSP ),
// repair; mostly doesn't use callbacks and sends responses as their own request messages, with matching sessions by uuid; should eventually harmonize and make idiomatic
- REPAIR_RSP (100, P1, repairTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
+ REPAIR_RSP (100, P1, repairTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, RESPONSE_HANDLER ),
VALIDATION_RSP (102, P1, longTimeout, ANTI_ENTROPY, () -> ValidationResponse.serializer, () -> RepairMessageVerbHandler.instance, REPAIR_RSP ),
VALIDATION_REQ (101, P1, repairTimeout, ANTI_ENTROPY, () -> ValidationRequest.serializer, () -> RepairMessageVerbHandler.instance, REPAIR_RSP ),
SYNC_RSP (104, P1, repairTimeout, ANTI_ENTROPY, () -> SyncResponse.serializer, () -> RepairMessageVerbHandler.instance, REPAIR_RSP ),
@@ -175,35 +191,69 @@ public enum Verb
STATUS_RSP (115, P1, repairTimeout, ANTI_ENTROPY, () -> StatusResponse.serializer, () -> RepairMessageVerbHandler.instance, REPAIR_RSP ),
STATUS_REQ (114, P1, repairTimeout, ANTI_ENTROPY, () -> StatusRequest.serializer, () -> RepairMessageVerbHandler.instance, REPAIR_RSP ),
- REPLICATION_DONE_RSP (82, P0, rpcTimeout, MISC, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
+ REPLICATION_DONE_RSP (82, P0, rpcTimeout, MISC, () -> NoPayload.serializer, RESPONSE_HANDLER ),
REPLICATION_DONE_REQ (22, P0, rpcTimeout, MISC, () -> NoPayload.serializer, () -> ReplicationDoneVerbHandler.instance, REPLICATION_DONE_RSP),
- SNAPSHOT_RSP (87, P0, rpcTimeout, MISC, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
+ SNAPSHOT_RSP (87, P0, rpcTimeout, MISC, () -> NoPayload.serializer, RESPONSE_HANDLER ),
SNAPSHOT_REQ (27, P0, rpcTimeout, MISC, () -> SnapshotCommand.serializer, () -> SnapshotVerbHandler.instance, SNAPSHOT_RSP ),
PAXOS2_COMMIT_REMOTE_REQ (38, P2, writeTimeout, MUTATION, () -> Mutation.serializer, () -> MutationVerbHandler.instance, MUTATION_RSP ),
- PAXOS2_COMMIT_REMOTE_RSP (39, P2, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
- PAXOS2_PREPARE_RSP (50, P2, writeTimeout, REQUEST_RESPONSE, () -> PaxosPrepare.responseSerializer, () -> ResponseVerbHandler.instance ),
+ PAXOS2_COMMIT_REMOTE_RSP (39, P2, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, RESPONSE_HANDLER ),
+ PAXOS2_PREPARE_RSP (50, P2, writeTimeout, REQUEST_RESPONSE, () -> PaxosPrepare.responseSerializer, RESPONSE_HANDLER ),
PAXOS2_PREPARE_REQ (40, P2, writeTimeout, MUTATION, () -> PaxosPrepare.requestSerializer, () -> PaxosPrepare.requestHandler, PAXOS2_PREPARE_RSP ),
- PAXOS2_PREPARE_REFRESH_RSP (51, P2, writeTimeout, REQUEST_RESPONSE, () -> PaxosPrepareRefresh.responseSerializer, () -> ResponseVerbHandler.instance ),
+ PAXOS2_PREPARE_REFRESH_RSP (51, P2, writeTimeout, REQUEST_RESPONSE, () -> PaxosPrepareRefresh.responseSerializer, RESPONSE_HANDLER ),
PAXOS2_PREPARE_REFRESH_REQ (41, P2, writeTimeout, MUTATION, () -> PaxosPrepareRefresh.requestSerializer, () -> PaxosPrepareRefresh.requestHandler, PAXOS2_PREPARE_REFRESH_RSP ),
- PAXOS2_PROPOSE_RSP (52, P2, writeTimeout, REQUEST_RESPONSE, () -> PaxosPropose.responseSerializer, () -> ResponseVerbHandler.instance ),
+ PAXOS2_PROPOSE_RSP (52, P2, writeTimeout, REQUEST_RESPONSE, () -> PaxosPropose.responseSerializer, RESPONSE_HANDLER ),
PAXOS2_PROPOSE_REQ (42, P2, writeTimeout, MUTATION, () -> PaxosPropose.requestSerializer, () -> PaxosPropose.requestHandler, PAXOS2_PROPOSE_RSP ),
- PAXOS2_COMMIT_AND_PREPARE_RSP (53, P2, writeTimeout, REQUEST_RESPONSE, () -> PaxosPrepare.responseSerializer, () -> ResponseVerbHandler.instance ),
+ PAXOS2_COMMIT_AND_PREPARE_RSP (53, P2, writeTimeout, REQUEST_RESPONSE, () -> PaxosPrepare.responseSerializer, RESPONSE_HANDLER ),
PAXOS2_COMMIT_AND_PREPARE_REQ (43, P2, writeTimeout, MUTATION, () -> PaxosCommitAndPrepare.requestSerializer, () -> PaxosCommitAndPrepare.requestHandler, PAXOS2_COMMIT_AND_PREPARE_RSP ),
- PAXOS2_REPAIR_RSP (54, P2, writeTimeout, PAXOS_REPAIR, () -> PaxosRepair.responseSerializer, () -> ResponseVerbHandler.instance ),
+ PAXOS2_REPAIR_RSP (54, P2, writeTimeout, PAXOS_REPAIR, () -> PaxosRepair.responseSerializer, RESPONSE_HANDLER ),
PAXOS2_REPAIR_REQ (44, P2, writeTimeout, PAXOS_REPAIR, () -> PaxosRepair.requestSerializer, () -> PaxosRepair.requestHandler, PAXOS2_REPAIR_RSP ),
- PAXOS2_CLEANUP_START_PREPARE_RSP (55, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosCleanupHistory.serializer, () -> ResponseVerbHandler.instance ),
+ PAXOS2_CLEANUP_START_PREPARE_RSP (55, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosCleanupHistory.serializer, RESPONSE_HANDLER ),
PAXOS2_CLEANUP_START_PREPARE_REQ (45, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosStartPrepareCleanup.serializer, () -> PaxosStartPrepareCleanup.verbHandler, PAXOS2_CLEANUP_START_PREPARE_RSP ),
- PAXOS2_CLEANUP_RSP (56, P2, repairTimeout, PAXOS_REPAIR, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
+ PAXOS2_CLEANUP_RSP (56, P2, repairTimeout, PAXOS_REPAIR, () -> NoPayload.serializer, RESPONSE_HANDLER ),
PAXOS2_CLEANUP_REQ (46, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosCleanupRequest.serializer, () -> PaxosCleanupRequest.verbHandler, PAXOS2_CLEANUP_RSP ),
PAXOS2_CLEANUP_RSP2 (57, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosCleanupResponse.serializer, () -> PaxosCleanupResponse.verbHandler ),
- PAXOS2_CLEANUP_FINISH_PREPARE_RSP(58, P2, repairTimeout, PAXOS_REPAIR, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
+ PAXOS2_CLEANUP_FINISH_PREPARE_RSP(58, P2, repairTimeout, PAXOS_REPAIR, () -> NoPayload.serializer, RESPONSE_HANDLER ),
PAXOS2_CLEANUP_FINISH_PREPARE_REQ(47, P2, repairTimeout, IMMEDIATE, () -> PaxosCleanupHistory.serializer, () -> PaxosFinishPrepareCleanup.verbHandler, PAXOS2_CLEANUP_FINISH_PREPARE_RSP),
- PAXOS2_CLEANUP_COMPLETE_RSP (59, P2, repairTimeout, PAXOS_REPAIR, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ),
+ PAXOS2_CLEANUP_COMPLETE_RSP (59, P2, repairTimeout, PAXOS_REPAIR, () -> NoPayload.serializer, RESPONSE_HANDLER ),
PAXOS2_CLEANUP_COMPLETE_REQ (48, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosCleanupComplete.serializer, () -> PaxosCleanupComplete.verbHandler, PAXOS2_CLEANUP_COMPLETE_RSP ),
+ // accord
+ ACCORD_PREACCEPT_RSP (121, P2, writeTimeout, REQUEST_RESPONSE, () -> PreacceptSerializers.reply, RESPONSE_HANDLER),
+ ACCORD_PREACCEPT_REQ (120, P2, writeTimeout, ACCORD, () -> PreacceptSerializers.request, () -> AccordService.instance().verbHandler(), ACCORD_PREACCEPT_RSP),
+
+ ACCORD_ACCEPT_RSP (124, P2, writeTimeout, REQUEST_RESPONSE, () -> AcceptSerializers.reply, RESPONSE_HANDLER),
+ ACCORD_ACCEPT_REQ (122, P2, writeTimeout, ACCORD, () -> AcceptSerializers.request, () -> AccordService.instance().verbHandler(), ACCORD_ACCEPT_RSP ),
+ ACCORD_ACCEPT_INVALIDATE_REQ (123, P2, writeTimeout, ACCORD, () -> AcceptSerializers.invalidate, () -> AccordService.instance().verbHandler(), ACCORD_ACCEPT_RSP ),
+
+ ACCORD_READ_RSP (128, P2, writeTimeout, REQUEST_RESPONSE, () -> ReadDataSerializers.reply, RESPONSE_HANDLER),
+ ACCORD_READ_REQ (127, P2, writeTimeout, ACCORD, () -> ReadDataSerializers.request, () -> AccordService.instance().verbHandler(), ACCORD_READ_RSP ),
+ ACCORD_COMMIT_REQ (125, P2, writeTimeout, ACCORD, () -> CommitSerializers.request, () -> AccordService.instance().verbHandler(), ACCORD_READ_RSP ),
+ ACCORD_COMMIT_INVALIDATE_REQ (126, P2, writeTimeout, ACCORD, () -> CommitSerializers.invalidate, () -> AccordService.instance().verbHandler()),
+
+ ACCORD_APPLY_RSP (130, P2, writeTimeout, REQUEST_RESPONSE, () -> ApplySerializers.reply, RESPONSE_HANDLER),
+ ACCORD_APPLY_REQ (129, P2, writeTimeout, ACCORD, () -> ApplySerializers.request, () -> AccordService.instance().verbHandler(), ACCORD_APPLY_RSP),
+ ACCORD_APPLY_AND_CHECK_RSP (132, P2, writeTimeout, REQUEST_RESPONSE, () -> ApplyAndCheckSerializers.reply, RESPONSE_HANDLER),
+ ACCORD_APPLY_AND_CHECK_REQ (131, P2, writeTimeout, ACCORD, () -> ApplyAndCheckSerializers.request, () -> AccordService.instance().verbHandler(), ACCORD_APPLY_AND_CHECK_RSP),
+
+ ACCORD_RECOVER_RSP (134, P2, writeTimeout, REQUEST_RESPONSE, () -> RecoverySerializers.reply, RESPONSE_HANDLER),
+ ACCORD_RECOVER_REQ (133, P2, writeTimeout, ACCORD, () -> RecoverySerializers.request, () -> AccordService.instance().verbHandler(), ACCORD_RECOVER_RSP ),
+ ACCORD_BEGIN_INVALIDATE_RSP (136, P2, writeTimeout, REQUEST_RESPONSE, () -> BeginInvalidateSerializers.reply, RESPONSE_HANDLER),
+ ACCORD_BEGIN_INVALIDATE_REQ (135, P2, writeTimeout, ACCORD, () -> BeginInvalidateSerializers.request, () -> AccordService.instance().verbHandler(),ACCORD_BEGIN_INVALIDATE_RSP),
+ ACCORD_WAIT_COMMIT_RSP (138, P2, writeTimeout, REQUEST_RESPONSE, () -> WaitOnCommitSerializer.reply, RESPONSE_HANDLER),
+ ACCORD_WAIT_COMMIT_REQ (137, P2, writeTimeout, ACCORD, () -> WaitOnCommitSerializer.request, () -> AccordService.instance().verbHandler(), ACCORD_WAIT_COMMIT_RSP),
+
+ ACCORD_INFORM_OF_TXN_RSP (140, P2, writeTimeout, REQUEST_RESPONSE, () -> InformOfTxnSerializers.reply, RESPONSE_HANDLER),
+ ACCORD_INFORM_OF_TXN_REQ (139, P2, writeTimeout, ACCORD, () -> InformOfTxnSerializers.request, () -> AccordService.instance().verbHandler(), ACCORD_INFORM_OF_TXN_RSP),
+
+ ACCORD_INFORM_OF_PERSISTENCE_RSP(142, P2, writeTimeout, REQUEST_RESPONSE, () -> InformOfTxnSerializers.reply, RESPONSE_HANDLER),
+ ACCORD_INFORM_OF_PERSIETENCE_REQ(141, P2, writeTimeout, ACCORD, () -> InformOfPersistenceSerializers.request, () -> AccordService.instance().verbHandler(), ACCORD_INFORM_OF_TXN_RSP),
+
+ ACCORD_CHECK_STATUS_RSP (144, P2, writeTimeout, REQUEST_RESPONSE, () -> CheckStatusSerializers.reply, RESPONSE_HANDLER),
+ ACCORD_CHECK_STATUS_REQ (143, P2, writeTimeout, ACCORD, () -> CheckStatusSerializers.request, () -> AccordService.instance().verbHandler(), ACCORD_CHECK_STATUS_RSP),
+
// generic failure response
- FAILURE_RSP (99, P0, noTimeout, REQUEST_RESPONSE, () -> RequestFailureReason.serializer, () -> ResponseVerbHandler.instance ),
+ FAILURE_RSP (99, P0, noTimeout, REQUEST_RESPONSE, () -> RequestFailureReason.serializer, RESPONSE_HANDLER ),
// dummy verbs
_TRACE (30, P1, rpcTimeout, TRACING, () -> NoPayload.serializer, () -> null ),
@@ -212,9 +262,9 @@ public enum Verb
_TEST_2 (11, P1, rpcTimeout, IMMEDIATE, () -> NoPayload.serializer, () -> null ),
@Deprecated
- REQUEST_RSP (4, P1, rpcTimeout, REQUEST_RESPONSE, () -> null, () -> ResponseVerbHandler.instance ),
+ REQUEST_RSP (4, P1, rpcTimeout, REQUEST_RESPONSE, () -> null, RESPONSE_HANDLER ),
@Deprecated
- INTERNAL_RSP (23, P1, rpcTimeout, INTERNAL_RESPONSE, () -> null, () -> ResponseVerbHandler.instance ),
+ INTERNAL_RSP (23, P1, rpcTimeout, INTERNAL_RESPONSE, () -> null, RESPONSE_HANDLER ),
// largest used ID: 116
@@ -271,20 +321,30 @@ public enum Verb
*/
Verb(int id, Priority priority, ToLongFunction<TimeUnit> expiration, Stage stage, Supplier<? extends IVersionedAsymmetricSerializer<?, ?>> serializer, Supplier<? extends IVerbHandler<?>> handler)
{
- this(id, priority, expiration, stage, serializer, handler, null);
+ this(NORMAL, id, priority, expiration, stage, serializer, handler, null, null);
+ }
+
+ Verb(int id, Priority priority, ToLongFunction<TimeUnit> expiration, Stage stage, Supplier<? extends IVersionedAsymmetricSerializer<?, ?>> serializer, Supplier<? extends IVerbHandler<?>> handler, Predicate<?> isFinalReply)
+ {
+ this(NORMAL, id, priority, expiration, stage, serializer, handler, null, isFinalReply);
}
Verb(int id, Priority priority, ToLongFunction<TimeUnit> expiration, Stage stage, Supplier<? extends IVersionedAsymmetricSerializer<?, ?>> serializer, Supplier<? extends IVerbHandler<?>> handler, Verb responseVerb)
{
- this(NORMAL, id, priority, expiration, stage, serializer, handler, responseVerb);
+ this(NORMAL, id, priority, expiration, stage, serializer, handler, responseVerb, null);
}
Verb(Kind kind, int id, Priority priority, ToLongFunction<TimeUnit> expiration, Stage stage, Supplier<? extends IVersionedAsymmetricSerializer<?, ?>> serializer, Supplier<? extends IVerbHandler<?>> handler)
{
- this(kind, id, priority, expiration, stage, serializer, handler, null);
+ this(kind, id, priority, expiration, stage, serializer, handler, null, null);
+ }
+
+ Verb(Kind kind, int id, Priority priority, ToLongFunction<TimeUnit> expiration, Stage stage, Supplier<? extends IVersionedAsymmetricSerializer<?, ?>> serializer, Supplier<? extends IVerbHandler<?>> handler, Predicate<?> isFinalReply)
+ {
+ this(kind, id, priority, expiration, stage, serializer, handler, null, isFinalReply);
}
- Verb(Kind kind, int id, Priority priority, ToLongFunction<TimeUnit> expiration, Stage stage, Supplier<? extends IVersionedAsymmetricSerializer<?, ?>> serializer, Supplier<? extends IVerbHandler<?>> handler, Verb responseVerb)
+ Verb(Kind kind, int id, Priority priority, ToLongFunction<TimeUnit> expiration, Stage stage, Supplier<? extends IVersionedAsymmetricSerializer<?, ?>> serializer, Supplier<? extends IVerbHandler<?>> handler, Verb responseVerb, Predicate<?> isFinalReply)
{
this.stage = stage;
if (id < 0)
@@ -333,7 +393,7 @@ public enum Verb
// this is a little hacky, but reduces the number of parameters up top
public boolean isResponse()
{
- return handler.get() == ResponseVerbHandler.instance;
+ return handler == RESPONSE_HANDLER;
}
Verb toPre40Verb()
@@ -484,3 +544,8 @@ class VerbTimeouts
static final ToLongFunction<TimeUnit> longTimeout = units -> Math.max(DatabaseDescriptor.getRpcTimeout(units), units.convert(5L, TimeUnit.MINUTES));
static final ToLongFunction<TimeUnit> noTimeout = units -> { throw new IllegalStateException(); };
}
+
+class ResponseHandlerSupplier
+{
+ static final Supplier<IVerbHandler<?>> RESPONSE_HANDLER = () -> ResponseVerbHandler.instance;
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/schema/Schema.java b/src/java/org/apache/cassandra/schema/Schema.java
index f23370c549..6907010a4d 100644
--- a/src/java/org/apache/cassandra/schema/Schema.java
+++ b/src/java/org/apache/cassandra/schema/Schema.java
@@ -45,6 +45,7 @@ import org.apache.cassandra.utils.concurrent.LoadingMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.apache.cassandra.service.accord.AccordKeyspace;
import static java.lang.String.format;
@@ -105,7 +106,7 @@ public class Schema implements SchemaProvider
{
this.online = isDaemonInitialized();
this.localKeyspaces = (FORCE_LOAD_LOCAL_KEYSPACES || isDaemonInitialized() || isToolInitialized())
- ? Keyspaces.of(SchemaKeyspace.metadata(), SystemKeyspace.metadata())
+ ? Keyspaces.of(SchemaKeyspace.metadata(), SystemKeyspace.metadata(), AccordKeyspace.metadata())
: Keyspaces.none();
this.localKeyspaces.forEach(this::loadNew);
diff --git a/src/java/org/apache/cassandra/schema/SchemaConstants.java b/src/java/org/apache/cassandra/schema/SchemaConstants.java
index 888ea4a31f..62aae024a4 100644
--- a/src/java/org/apache/cassandra/schema/SchemaConstants.java
+++ b/src/java/org/apache/cassandra/schema/SchemaConstants.java
@@ -40,6 +40,7 @@ public final class SchemaConstants
public static final String SCHEMA_KEYSPACE_NAME = "system_schema";
public static final String TRACE_KEYSPACE_NAME = "system_traces";
+ public static final String ACCORD_KEYSPACE_NAME = "system_accord";
public static final String AUTH_KEYSPACE_NAME = "system_auth";
public static final String DISTRIBUTED_KEYSPACE_NAME = "system_distributed";
@@ -49,7 +50,7 @@ public final class SchemaConstants
/* system keyspace names (the ones with LocalStrategy replication strategy) */
public static final Set<String> LOCAL_SYSTEM_KEYSPACE_NAMES =
- ImmutableSet.of(SYSTEM_KEYSPACE_NAME, SCHEMA_KEYSPACE_NAME);
+ ImmutableSet.of(SYSTEM_KEYSPACE_NAME, SCHEMA_KEYSPACE_NAME, ACCORD_KEYSPACE_NAME);
/* virtual table system keyspace names */
public static final Set<String> VIRTUAL_SYSTEM_KEYSPACE_NAMES =
diff --git a/src/java/org/apache/cassandra/schema/TableId.java b/src/java/org/apache/cassandra/schema/TableId.java
index fd47a47e58..53b4465bf3 100644
--- a/src/java/org/apache/cassandra/schema/TableId.java
+++ b/src/java/org/apache/cassandra/schema/TableId.java
@@ -24,6 +24,8 @@ import java.util.UUID;
import org.apache.commons.lang3.ArrayUtils;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.db.marshal.ValueAccessor;
import org.apache.cassandra.utils.ByteBufferUtil;
import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID;
@@ -36,7 +38,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
* This is essentially a UUID, but we wrap it as it's used quite a bit in the code and having a nicely named class make
* the code more readable.
*/
-public class TableId
+public class TableId implements Comparable<TableId>
{
// TODO: should this be a TimeUUID?
private final UUID id;
@@ -91,6 +93,12 @@ public class TableId
return id;
}
+ @Override
+ public int compareTo(TableId that)
+ {
+ return this.id.compareTo(that.id);
+ }
+
@Override
public final int hashCode()
{
@@ -115,7 +123,15 @@ public class TableId
out.writeLong(id.getLeastSignificantBits());
}
- public int serializedSize()
+ public <V> int serialize(V dst, ValueAccessor<V> accessor, int offset)
+ {
+ int position = offset;
+ position += accessor.putLong(dst, position, id.getMostSignificantBits());
+ position += accessor.putLong(dst, position, id.getLeastSignificantBits());
+ return position - offset;
+ }
+
+ public static int serializedSize()
{
return 16;
}
@@ -124,4 +140,9 @@ public class TableId
{
return new TableId(new UUID(in.readLong(), in.readLong()));
}
+
+ public static <V> TableId deserialize(V src, ValueAccessor<V> accessor, int offset) throws IOException
+ {
+ return new TableId(new UUID(accessor.getLong(src, offset), accessor.getLong(src, offset + TypeSizes.LONG_SIZE)));
+ }
}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordCallback.java b/src/java/org/apache/cassandra/service/accord/AccordCallback.java
new file mode 100644
index 0000000000..60b5d6988a
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordCallback.java
@@ -0,0 +1,69 @@
+/*
+ * 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.coordinate.Timeout;
+import accord.messages.Callback;
+import accord.messages.Reply;
+import org.apache.cassandra.exceptions.RequestFailureReason;
+import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.net.Message;
+import org.apache.cassandra.net.RequestCallback;
+
+class AccordCallback<T extends Reply> implements RequestCallback<T>
+{
+ private static final Logger logger = LoggerFactory.getLogger(AccordCallback.class);
+ private final Callback<T> callback;
+
+ public AccordCallback(Callback<T> callback)
+ {
+ this.callback = callback;
+ }
+
+ @Override
+ public void onResponse(Message<T> msg)
+ {
+ logger.debug("Received response {} from {}", msg.payload, msg.from());
+ callback.onSuccess(EndpointMapping.endpointToId(msg.from()), msg.payload);
+ }
+
+ private static Throwable convertReason(RequestFailureReason reason)
+ {
+ return reason == RequestFailureReason.TIMEOUT ?
+ new Timeout(null, null) :
+ new RuntimeException(reason.toString());
+ }
+
+ @Override
+ public void onFailure(InetAddressAndPort from, RequestFailureReason failureReason)
+ {
+ logger.debug("Received failure {} from {} for {}", failureReason, from, callback);
+ // TODO (now): we should distinguish timeout failures with some placeholder Exception
+ callback.onFailure(EndpointMapping.endpointToId(from), convertReason(failureReason));
+ }
+
+ @Override
+ public boolean invokeOnFailure()
+ {
+ return true;
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordCommand.java b/src/java/org/apache/cassandra/service/accord/AccordCommand.java
new file mode 100644
index 0000000000..3c373e60df
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordCommand.java
@@ -0,0 +1,788 @@
+/*
+ * 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.nio.ByteBuffer;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.base.Preconditions;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.api.Data;
+import accord.api.Key;
+import accord.api.Result;
+import accord.local.Command;
+import accord.local.Listener;
+import accord.local.Listeners;
+import accord.local.PartialCommand;
+import accord.local.PreLoadContext;
+import accord.local.Status;
+import accord.primitives.Ballot;
+import accord.primitives.Deps;
+import accord.primitives.KeyRanges;
+import accord.primitives.Keys;
+import accord.primitives.Timestamp;
+import accord.primitives.TxnId;
+import accord.txn.Txn;
+import accord.txn.Writes;
+import accord.utils.DeterministicIdentitySet;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.async.AsyncContext;
+import org.apache.cassandra.service.accord.db.AccordData;
+import org.apache.cassandra.service.accord.store.StoredBoolean;
+import org.apache.cassandra.service.accord.store.StoredNavigableMap;
+import org.apache.cassandra.service.accord.store.StoredSet;
+import org.apache.cassandra.service.accord.store.StoredValue;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.concurrent.AsyncPromise;
+import org.apache.cassandra.utils.concurrent.Future;
+
+import static org.apache.cassandra.service.accord.AccordState.WriteOnly.applyMapChanges;
+import static org.apache.cassandra.service.accord.AccordState.WriteOnly.applySetChanges;
+
+public class AccordCommand extends Command implements AccordState<TxnId>
+{
+ private static final Logger logger = LoggerFactory.getLogger(AccordCommand.class);
+
+ private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger(0);
+
+ private static final long EMPTY_SIZE = ObjectSizes.measure(new AccordCommand(null, null));
+
+ public static class WriteOnly extends AccordCommand implements AccordState.WriteOnly<TxnId, AccordCommand>
+ {
+ private Future<?> future = null;
+
+ public WriteOnly(AccordCommandStore commandStore, TxnId txnId)
+ {
+ super(commandStore, txnId);
+ }
+
+ @Override
+ public void future(Future<?> future)
+ {
+ Preconditions.checkArgument(this.future == null);
+ this.future = future;
+ }
+
+ @Override
+ public Future<?> future()
+ {
+ return future;
+ }
+
+ @Override
+ public void applyChanges(AccordCommand instance)
+ {
+ applyMapChanges(this, instance, cmd -> cmd.waitingOnCommit);
+ applyMapChanges(this, instance, cmd -> cmd.waitingOnApply);
+ applySetChanges(this, instance, cmd -> cmd.blockingCommitOn);
+ applySetChanges(this, instance, cmd -> cmd.blockingApplyOn);
+ }
+ }
+
+ public static class ReadOnly extends AccordCommand implements AccordState.ReadOnly<TxnId, AccordCommand>
+ {
+ public ReadOnly(AccordCommandStore commandStore, TxnId txnId)
+ {
+ super(commandStore, txnId);
+ }
+
+ @Override
+ boolean isReadOnly()
+ {
+ return true;
+ }
+ }
+
+ private final AccordCommandStore commandStore;
+ private final TxnId txnId;
+ private final int instanceCount = INSTANCE_COUNTER.getAndIncrement();
+ public final StoredValue<Key> homeKey;
+ public final StoredValue<Key> progressKey;
+ public final StoredValue<Txn> txn;
+ public final StoredValue<Ballot> promised;
+ public final StoredValue<Ballot> accepted;
+ public final StoredValue<Timestamp> executeAt;
+ public final StoredValue<Deps> deps;
+ public final StoredValue<Writes> writes;
+ public final StoredValue<Result> result;
+
+ public final StoredValue.HistoryPreserving<Status> status;
+ public final StoredBoolean isGloballyPersistent;
+
+ public final StoredNavigableMap<TxnId, ByteBuffer> waitingOnCommit;
+ public final StoredNavigableMap<TxnId, ByteBuffer> waitingOnApply;
+
+ public final StoredSet.DeterministicIdentity<ListenerProxy> storedListeners;
+ private final Listeners transientListeners;
+
+ public final StoredSet.Navigable<TxnId> blockingCommitOn;
+ public final StoredSet.Navigable<TxnId> blockingApplyOn;
+
+ public AccordCommand(AccordCommandStore commandStore, TxnId txnId)
+ {
+ logger.trace("Instantiating new command {} @ {}", txnId, instanceHash());
+ this.commandStore = commandStore;
+ this.txnId = txnId;
+ homeKey = new StoredValue<>(kind());
+ progressKey = new StoredValue<>(kind());
+ txn = new StoredValue<>(kind());
+ promised = new StoredValue<>(kind());
+ accepted = new StoredValue<>(kind());
+ executeAt = new StoredValue<>(kind());
+ deps = new StoredValue<>(kind());
+ writes = new StoredValue<>(kind());
+ result = new StoredValue<>(kind());
+ status = new StoredValue.HistoryPreserving<>(kind());
+ isGloballyPersistent = new StoredBoolean(kind());
+ waitingOnCommit = new StoredNavigableMap<>(kind());
+ waitingOnApply = new StoredNavigableMap<>(kind());
+ storedListeners = new StoredSet.DeterministicIdentity<>(kind());
+ transientListeners = new Listeners();
+ blockingCommitOn = new StoredSet.Navigable<>(kind());
+ blockingApplyOn = new StoredSet.Navigable<>(kind());
+ }
+
+
+
+
+ @Override
+ public String toString()
+ {
+ return "AccordCommand{" +
+ "txnId=" + txnId +
+ ", instanceHash=" + instanceHash() +
+ ", status=" + status +
+ ", executeAt=" + executeAt +
+ ", promised=" + promised +
+ ", accepted=" + accepted +
+// ", deps=" + deps +
+// ", homeKey=" + homeKey +
+// ", progressKey=" + progressKey +
+// ", txn=" + txn +
+// ", writes=" + writes +
+// ", result=" + result +
+ ", isGloballyPersistent=" + isGloballyPersistent +
+ ", waitingOnCommit=" + waitingOnCommit +
+ ", waitingOnApply=" + waitingOnApply +
+ ", storedListeners=" + storedListeners +
+ ", transientListeners=" + transientListeners +
+ ", blockingCommitOn=" + blockingCommitOn +
+ ", blockingApplyOn=" + blockingApplyOn +
+ '}';
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ return homeKey.isEmpty()
+ || progressKey.isEmpty()
+ || txn.isEmpty()
+ || promised.isEmpty()
+ || accepted.isEmpty()
+ || executeAt.isEmpty()
+ || deps.isEmpty()
+ || writes.isEmpty()
+ || result.isEmpty()
+ || status.isEmpty()
+ || isGloballyPersistent.isEmpty()
+ || waitingOnCommit.isEmpty()
+ || blockingCommitOn.isEmpty()
+ || waitingOnApply.isEmpty()
+ || blockingApplyOn.isEmpty()
+ || storedListeners.isEmpty();
+ }
+
+ public void setEmpty()
+ {
+ homeKey.setEmpty();
+ progressKey.setEmpty();
+ txn.setEmpty();
+ promised.setEmpty();
+ accepted.setEmpty();
+ executeAt.setEmpty();
+ deps.setEmpty();
+ writes.setEmpty();
+ result.setEmpty();
+ status.setEmpty();
+ isGloballyPersistent.setEmpty();
+ waitingOnCommit.setEmpty();
+ blockingCommitOn.setEmpty();
+ waitingOnApply.setEmpty();
+ blockingApplyOn.setEmpty();
+ storedListeners.setEmpty();;
+ }
+
+ public AccordCommand initialize()
+ {
+ logger.trace("Initializing command {} @ {}", txnId, instanceHash());
+ status.set(Status.NotWitnessed);
+ homeKey.set(null);
+ progressKey.set(null);
+ txn.set(null);
+ executeAt.load(null);
+ promised.set(Ballot.ZERO);
+ accepted.set(Ballot.ZERO);
+ deps.set(Deps.NONE);
+ writes.load(null);
+ result.load(null);
+ isGloballyPersistent.set(false);
+ waitingOnCommit.load(new TreeMap<>());
+ blockingCommitOn.load(new TreeSet<>());
+ waitingOnApply.load(new TreeMap<>());
+ blockingApplyOn.load(new TreeSet<>());
+ storedListeners.load(new DeterministicIdentitySet<>());
+ return this;
+ }
+
+ @Override
+ public boolean isLoaded()
+ {
+ return homeKey.isLoaded()
+ && progressKey.isLoaded()
+ && txn.isLoaded()
+ && promised.isLoaded()
+ && accepted.isLoaded()
+ && executeAt.isLoaded()
+ && deps.isLoaded()
+ && writes.isLoaded()
+ && result.isLoaded()
+ && status.isLoaded()
+ && isGloballyPersistent.isLoaded()
+ && waitingOnCommit.isLoaded()
+ && blockingCommitOn.isLoaded()
+ && waitingOnApply.isLoaded()
+ && blockingApplyOn.isLoaded()
+ && storedListeners.isLoaded();
+ }
+
+ public boolean isPartiallyLoaded()
+ {
+ return homeKey.isLoaded()
+ || progressKey.isLoaded()
+ || txn.isLoaded()
+ || promised.isLoaded()
+ || accepted.isLoaded()
+ || executeAt.isLoaded()
+ || deps.isLoaded()
+ || writes.isLoaded()
+ || result.isLoaded()
+ || status.isLoaded()
+ || isGloballyPersistent.isLoaded()
+ || waitingOnCommit.isLoaded()
+ || blockingCommitOn.isLoaded()
+ || waitingOnApply.isLoaded()
+ || blockingApplyOn.isLoaded()
+ || storedListeners.isLoaded();
+ }
+
+ @Override
+ public boolean hasModifications()
+ {
+ return homeKey.hasModifications()
+ || progressKey.hasModifications()
+ || txn.hasModifications()
+ || promised.hasModifications()
+ || accepted.hasModifications()
+ || executeAt.hasModifications()
+ || deps.hasModifications()
+ || writes.hasModifications()
+ || result.hasModifications()
+ || status.hasModifications()
+ || isGloballyPersistent.hasModifications()
+ || waitingOnCommit.hasModifications()
+ || blockingCommitOn.hasModifications()
+ || waitingOnApply.hasModifications()
+ || blockingApplyOn.hasModifications()
+ || storedListeners.hasModifications();
+ }
+
+ @Override
+ public void clearModifiedFlag()
+ {
+ logger.trace("Clearing modified flag on command {} @ {}", txnId, instanceHash());
+ homeKey.clearModifiedFlag();
+ progressKey.clearModifiedFlag();
+ txn.clearModifiedFlag();
+ promised.clearModifiedFlag();
+ accepted.clearModifiedFlag();
+ executeAt.clearModifiedFlag();
+ deps.clearModifiedFlag();
+ writes.clearModifiedFlag();
+ result.clearModifiedFlag();
+ status.clearModifiedFlag();
+ isGloballyPersistent.clearModifiedFlag();
+ waitingOnCommit.clearModifiedFlag();
+ blockingCommitOn.clearModifiedFlag();
+ waitingOnApply.clearModifiedFlag();
+ blockingApplyOn.clearModifiedFlag();
+ storedListeners.clearModifiedFlag();;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AccordCommand command = (AccordCommand) o;
+ return commandStore == command.commandStore
+ && homeKey.equals(command.homeKey)
+ && progressKey.equals(command.progressKey)
+ && txnId.equals(command.txnId)
+ && txn.equals(command.txn)
+ && promised.equals(command.promised)
+ && accepted.equals(command.accepted)
+ && executeAt.equals(command.executeAt)
+ && deps.equals(command.deps)
+ && writes.equals(command.writes)
+ && result.equals(command.result)
+ && status.equals(command.status)
+ && isGloballyPersistent.equals(command.isGloballyPersistent)
+ && waitingOnCommit.equals(command.waitingOnCommit)
+ && blockingCommitOn.equals(command.blockingCommitOn)
+ && waitingOnApply.equals(command.waitingOnApply)
+ && blockingApplyOn.equals(command.blockingApplyOn)
+ && storedListeners.equals(command.storedListeners)
+ && transientListeners.equals(command.transientListeners);
+ }
+
+ boolean isReadOnly()
+ {
+ return false;
+ }
+
+ private int instanceHash()
+ {
+// return System.identityHashCode(this);
+ return instanceCount;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(commandStore,
+ txnId,
+ homeKey,
+ progressKey,
+ txn,
+ promised,
+ accepted,
+ executeAt,
+ deps,
+ writes,
+ result,
+ status,
+ isGloballyPersistent,
+ waitingOnCommit,
+ blockingCommitOn,
+ waitingOnApply,
+ blockingApplyOn,
+ storedListeners,
+ transientListeners);
+ }
+
+ private AccordStateCache.Instance<TxnId, AccordCommand> cache()
+ {
+ return commandStore.commandCache();
+ }
+
+ @Override
+ public TxnId key()
+ {
+ return txnId;
+ }
+
+ @Override
+ public long estimatedSizeOnHeap()
+ {
+ long size = EMPTY_SIZE;
+ size += AccordObjectSizes.timestamp(txnId);
+ size += homeKey.estimatedSizeOnHeap(AccordObjectSizes::key);
+ size += progressKey.estimatedSizeOnHeap(AccordObjectSizes::key);
+ size += txn.estimatedSizeOnHeap(AccordObjectSizes::txn);
+ size += promised.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+ size += accepted.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+ size += executeAt.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+ size += deps.estimatedSizeOnHeap(AccordObjectSizes::dependencies);
+ size += writes.estimatedSizeOnHeap(AccordObjectSizes::writes);
+ size += result.estimatedSizeOnHeap(r -> ((AccordData) r).estimatedSizeOnHeap());
+ size += status.estimatedSizeOnHeap(s -> 0);
+ size += isGloballyPersistent.estimatedSizeOnHeap();
+ size += waitingOnCommit.estimatedSizeOnHeap(AccordObjectSizes::timestamp, ByteBufferUtil::estimatedSizeOnHeap);
+ size += blockingCommitOn.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+ size += waitingOnApply.estimatedSizeOnHeap(AccordObjectSizes::timestamp, ByteBufferUtil::estimatedSizeOnHeap);
+ size += blockingApplyOn.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+ size += storedListeners.estimatedSizeOnHeap(ListenerProxy::estimatedSizeOnHeap);
+ return size;
+ }
+
+ public boolean shouldUpdateDenormalizedWaitingOn()
+ {
+ if (blockingCommitOn.getView().isEmpty() && blockingApplyOn.getView().isEmpty())
+ return false;
+ return AccordPartialCommand.serializer.needsUpdate(this);
+ }
+
+ @Override
+ public TxnId txnId()
+ {
+ return txnId;
+ }
+
+ @Override
+ public AccordCommandStore commandStore()
+ {
+ return commandStore;
+ }
+
+ @Override
+ public Key homeKey()
+ {
+ return homeKey.get();
+ }
+
+ @Override
+ protected void setHomeKey(Key key)
+ {
+ homeKey.set(key);
+ }
+
+ @Override
+ public Key progressKey()
+ {
+ return progressKey.get();
+ }
+
+ @Override
+ protected void setProgressKey(Key key)
+ {
+ progressKey.set(key);
+ }
+
+ @Override
+ public Txn txn()
+ {
+ return txn.get();
+ }
+
+ @Override
+ protected void setTxn(Txn txn)
+ {
+ this.txn.set(txn);
+ }
+
+ @Override
+ public Ballot promised()
+ {
+ return promised.get();
+ }
+
+ @Override
+ public void promised(Ballot ballot)
+ {
+ this.promised.set(ballot);
+ }
+
+ @Override
+ public Ballot accepted()
+ {
+ return accepted.get();
+ }
+
+ @Override
+ public void accepted(Ballot ballot)
+ {
+ this.accepted.set(ballot);
+ }
+
+ @Override
+ public Timestamp executeAt()
+ {
+ return executeAt.get();
+ }
+
+ @Override
+ public void executeAt(Timestamp timestamp)
+ {
+ Preconditions.checkState(!status().hasBeen(Status.Committed) || executeAt().equals(timestamp));
+ this.executeAt.set(timestamp);
+ }
+
+ @Override
+ public Deps savedDeps()
+ {
+ return deps.get();
+ }
+
+ @Override
+ public void savedDeps(Deps deps)
+ {
+ this.deps.set(deps);
+ }
+
+ @Override
+ public Writes writes()
+ {
+ return writes.get();
+ }
+
+ @Override
+ public void writes(Writes writes)
+ {
+ this.writes.set(writes);
+ }
+
+ @Override
+ public Result result()
+ {
+ return result.get();
+ }
+
+ @Override
+ public void result(Result result)
+ {
+ this.result.set(result);
+ }
+
+ @Override
+ public Status status()
+ {
+ return status.get();
+ }
+
+ @Override
+ public void status(Status status)
+ {
+ this.status.set(status);
+ }
+
+ @Override
+ public boolean isGloballyPersistent()
+ {
+ return isGloballyPersistent.get();
+ }
+
+ @Override
+ public void isGloballyPersistent(boolean v)
+ {
+ isGloballyPersistent.set(v);
+ }
+
+ @Override
+ protected void postApply()
+ {
+ cache().cleanupWriteFuture(txnId);
+ super.postApply();
+ }
+
+ private boolean canApplyWithCurrentScope()
+ {
+ KeyRanges ranges = commandStore.ranges().at(executeAt().epoch);
+ Keys keys = txn().keys();
+ for (int i=0,mi=keys.size(); i<mi; i++)
+ {
+ Key key = keys.get(i);
+ if (commandStore.isCommandsForKeyInContext((AccordKey.PartitionKey) key))
+ continue;
+
+ if (!commandStore.hashIntersects(key))
+ continue;
+ if (!ranges.contains(key))
+ continue;
+
+ return false;
+ }
+ return true;
+ }
+
+ private Future<Void> applyWithCorrectScope()
+ {
+ TxnId txnId = txnId();
+ AsyncPromise<Void> promise = new AsyncPromise<>();
+ commandStore().process(this, commandStore -> {
+ AccordCommand command = (AccordCommand) commandStore.command(txnId);
+ command.apply(false).addCallback((v, throwable) -> {
+ if (throwable != null)
+ promise.tryFailure(throwable);
+ else
+ promise.trySuccess(null);
+ });
+ });
+ return promise;
+ }
+
+ private Future<Void> apply(boolean canReschedule)
+ {
+ Future<Void> future = cache().getWriteFuture(txnId);
+ if (future != null)
+ return future;
+
+ // this can be called via a listener callback, in which case we won't
+ // have the appropriate commandsForKey in scope, so start a new operation
+ // with the correct scope and notify the caller when that completes
+ if (!canApplyWithCurrentScope())
+ {
+ Preconditions.checkArgument(canReschedule);
+ return applyWithCorrectScope();
+ }
+
+ future = super.apply();
+ cache().setWriteFuture(txnId, future);
+ return future;
+ }
+
+ @Override
+ public Future<Void> apply()
+ {
+ return apply(true);
+ }
+
+ @Override
+ public Future<Data> read(Keys scope)
+ {
+ ReadFuture future = cache().getReadFuture(txnId);
+ if (future != null)
+ return future.scope.equals(scope) ? future : super.read(scope);
+ future = new ReadFuture(scope, super.read(scope));
+ cache().setReadFuture(txnId, future);
+ return future;
+ }
+
+ private Listener maybeWrapListener(Listener listener)
+ {
+ if (listener.isTransient())
+ return listener;
+
+ if (listener instanceof AccordCommand)
+ return new ListenerProxy.CommandListenerProxy(commandStore, ((AccordCommand) listener).txnId());
+
+ if (listener instanceof AccordCommandsForKey)
+ return new ListenerProxy.CommandsForKeyListenerProxy(commandStore, ((AccordCommandsForKey) listener).key());
+
+ throw new RuntimeException("Unhandled non-transient listener: " + listener);
+ }
+
+ @Override
+ public Command addListener(Listener listener)
+ {
+ listener = maybeWrapListener(listener);
+ if (listener instanceof ListenerProxy)
+ storedListeners.blindAdd((ListenerProxy) listener);
+ else
+ transientListeners.add(listener);
+ return this;
+ }
+
+ @Override
+ public void removeListener(Listener listener)
+ {
+ listener = maybeWrapListener(listener);
+ if (listener instanceof ListenerProxy)
+ storedListeners.blindRemove((ListenerProxy) listener);
+ else
+ transientListeners.remove(listener);
+ }
+
+ public boolean hasListenerFor(TxnId txnId)
+ {
+ return storedListeners.getView().contains(new ListenerProxy.CommandListenerProxy(commandStore, txnId));
+ }
+
+ @Override
+ public void notifyListeners()
+ {
+ storedListeners.getView().forEach(this);
+ transientListeners.forEach(listener -> {
+ PreLoadContext ctx = listener.listenerPreLoadContext(txnId());
+ AsyncContext context = commandStore().getContext();
+ if (context.containsScopedItems(ctx))
+ {
+ logger.trace("{}: synchronously updating listener {}", txnId(), listener);
+ listener.onChange(this);
+ }
+ else
+ {
+ logger.trace("{}: asynchronously updating listener {}", txnId(), listener);
+ commandStore().process(ctx, instance -> {
+ listener.onChange(instance.command(txnId()));
+ });
+ }
+ });
+ }
+
+ @Override
+ public void addWaitingOnCommit(Command command)
+ {
+ waitingOnCommit.blindPut(command.txnId(), AccordPartialCommand.serializer.serialize(command));
+ }
+
+ @Override
+ public boolean isWaitingOnCommit()
+ {
+ return !waitingOnCommit.getView().isEmpty();
+ }
+
+ @Override
+ public void removeWaitingOnCommit(PartialCommand command)
+ {
+ waitingOnCommit.blindRemove(command.txnId());
+ }
+
+ @Override
+ public PartialCommand firstWaitingOnCommit()
+ {
+ if (!isWaitingOnCommit())
+ return null;
+ ByteBuffer bytes = waitingOnCommit.getView().firstEntry().getValue();
+ return AccordPartialCommand.serializer.deserialize(commandStore, bytes);
+ }
+
+ @Override
+ public void addWaitingOnApplyIfAbsent(PartialCommand command)
+ {
+ waitingOnApply.blindPut(command.txnId(), AccordPartialCommand.serializer.serialize(command));
+ }
+
+ @Override
+ public boolean isWaitingOnApply()
+ {
+ return !waitingOnApply.getView().isEmpty();
+ }
+
+ @Override
+ public void removeWaitingOnApply(PartialCommand command)
+ {
+ waitingOnApply.blindRemove(command.txnId());
+ }
+
+ @Override
+ public PartialCommand firstWaitingOnApply()
+ {
+ if (!isWaitingOnApply())
+ return null;
+ ByteBuffer bytes = waitingOnApply.getView().firstEntry().getValue();
+ return AccordPartialCommand.serializer.deserialize(commandStore, bytes);
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordCommandStore.java b/src/java/org/apache/cassandra/service/accord/AccordCommandStore.java
new file mode 100644
index 0000000000..f8659e0b3c
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordCommandStore.java
@@ -0,0 +1,291 @@
+/*
+ * 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.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.LongSupplier;
+
+import com.google.common.base.Preconditions;
+
+import accord.api.Agent;
+import accord.api.DataStore;
+import accord.api.Key;
+import accord.api.ProgressLog;
+import accord.local.Command;
+import accord.local.CommandStore;
+import accord.local.CommandsForKey;
+import accord.local.PreLoadContext;
+import accord.primitives.Timestamp;
+import accord.primitives.TxnId;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.service.accord.async.AsyncContext;
+import org.apache.cassandra.service.accord.async.AsyncOperation;
+import org.apache.cassandra.utils.Clock;
+import org.apache.cassandra.utils.concurrent.AsyncPromise;
+import org.apache.cassandra.utils.concurrent.Future;
+import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
+
+public class AccordCommandStore extends CommandStore
+{
+ public static long maxCacheSize()
+ {
+ return 5 << 20; // TODO: make configurable
+ }
+
+ private static long getThreadId(ExecutorService executor)
+ {
+ try
+ {
+ return executor.submit(() -> Thread.currentThread().getId()).get();
+ }
+ catch (InterruptedException e)
+ {
+ throw new AssertionError(e);
+ }
+ catch (ExecutionException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private final long threadId;
+ public final String loggingId;
+ private final ExecutorService executor;
+ private final AccordStateCache stateCache;
+ private final AccordStateCache.Instance<TxnId, AccordCommand> commandCache;
+ private final AccordStateCache.Instance<PartitionKey, AccordCommandsForKey> commandsForKeyCache;
+ private AsyncContext currentCtx = null;
+ private long lastSystemTimestampMicros = Long.MIN_VALUE;
+
+ public AccordCommandStore(int generation,
+ int index,
+ int numShards,
+ Function<Timestamp, Timestamp> uniqueNow,
+ LongSupplier currentEpoch,
+ Agent agent,
+ DataStore store,
+ ProgressLog.Factory progressLogFactory,
+ RangesForEpoch rangesForEpoch,
+ ExecutorService executor)
+ {
+ super(generation, index, numShards, uniqueNow, currentEpoch, agent, store, progressLogFactory, rangesForEpoch);
+ this.loggingId = String.format("[%s:%s]", generation, index);
+ this.executor = executor;
+ this.threadId = getThreadId(executor);
+ this.stateCache = new AccordStateCache(maxCacheSize() / numShards);
+ this.commandCache = stateCache.instance(TxnId.class,
+ AccordCommand.class,
+ txnId -> new AccordCommand(this, txnId));
+ this.commandsForKeyCache = stateCache.instance(PartitionKey.class,
+ AccordCommandsForKey.class,
+ key -> new AccordCommandsForKey(this, key));
+ }
+
+ void setCacheSize(long bytes)
+ {
+ checkInStoreThread();
+ stateCache.setMaxSize(bytes);
+ }
+
+ public void checkInStoreThread()
+ {
+ Preconditions.checkState(Thread.currentThread().getId() == threadId);
+ }
+
+ public void checkNotInStoreThread()
+ {
+ Preconditions.checkState(Thread.currentThread().getId() != threadId);
+ }
+
+ public ExecutorService executor()
+ {
+ return executor;
+ }
+
+ public AccordStateCache.Instance<TxnId, AccordCommand> commandCache()
+ {
+ return commandCache;
+ }
+
+ public AccordStateCache.Instance<PartitionKey, AccordCommandsForKey> commandsForKeyCache()
+ {
+ return commandsForKeyCache;
+ }
+
+ public void setContext(AsyncContext context)
+ {
+ Preconditions.checkState(currentCtx == null);
+ currentCtx = context;
+ }
+
+ public AsyncContext getContext()
+ {
+ Preconditions.checkState(currentCtx != null);
+ return currentCtx;
+ }
+
+ public void unsetContext(AsyncContext context)
+ {
+ Preconditions.checkState(currentCtx == context);
+ currentCtx = null;
+ }
+
+ public long nextSystemTimestampMicros()
+ {
+ lastSystemTimestampMicros = Math.max(TimeUnit.MILLISECONDS.toMicros(Clock.Global.currentTimeMillis()), lastSystemTimestampMicros + 1);
+ return lastSystemTimestampMicros;
+ }
+
+ private AccordCommand getCommandInternal(TxnId txnId)
+ {
+ Preconditions.checkState(currentCtx != null);
+ AccordCommand command = currentCtx.commands.get(txnId);
+ if (command == null)
+ throw new IllegalArgumentException("No command in context for txnId " + txnId);
+
+ Preconditions.checkState(command.isLoaded() || (command.isReadOnly() && command.isPartiallyLoaded()));
+
+ return command;
+ }
+
+ @Override
+ public Command command(TxnId txnId)
+ {
+ AccordCommand command = getCommandInternal(txnId);
+ if (command.isEmpty()) command.initialize();
+ return command;
+ }
+
+ @Override
+ public Command ifPresent(TxnId txnId)
+ {
+ AccordCommand command = getCommandInternal(txnId);
+ return !command.isEmpty() ? command : null;
+ }
+
+ public boolean isCommandsForKeyInContext(PartitionKey key)
+ {
+ return currentCtx.commandsForKey.get(key) != null;
+ }
+
+ private AccordCommandsForKey getCommandsForKeyInternal(Key key)
+ {
+ Preconditions.checkState(currentCtx != null);
+ Preconditions.checkArgument(key instanceof PartitionKey);
+ AccordCommandsForKey commandsForKey = currentCtx.commandsForKey.get((PartitionKey) key);
+ if (commandsForKey == null)
+ throw new IllegalArgumentException("No commandsForKey in context for key " + key);
+ Preconditions.checkState(commandsForKey.isLoaded());
+ return commandsForKey;
+ }
+
+ @Override
+ public CommandsForKey commandsForKey(Key key)
+ {
+ AccordCommandsForKey commandsForKey = getCommandsForKeyInternal(key);
+ if (commandsForKey.isEmpty())
+ commandsForKey.initialize();
+ return commandsForKey;
+ }
+
+ @Override
+ public CommandsForKey maybeCommandsForKey(Key key)
+ {
+ AccordCommandsForKey commandsForKey = getCommandsForKeyInternal(key);
+ return !commandsForKey.isEmpty() ? commandsForKey : null;
+ }
+
+ @Override
+ public Future<Void> processSetup(Consumer<? super CommandStore> function)
+ {
+ AsyncPromise<Void> promise = new AsyncPromise<>();
+ executor.execute(() -> {
+ try
+ {
+ function.accept(this);
+ promise.trySuccess(null);
+ }
+ catch (Throwable t)
+ {
+ promise.tryFailure(t);
+ }
+ });
+ return promise;
+ }
+
+ @Override
+ public <T> Future<T> processSetup(Function<? super CommandStore, T> function)
+ {
+ AsyncPromise<T> promise = new AsyncPromise<>();
+ executor.execute(() -> {
+ try
+ {
+ T result = function.apply(this);
+ promise.trySuccess(result);
+ }
+ catch (Throwable t)
+ {
+ promise.tryFailure(t);
+ }
+ });
+ return promise;
+ }
+
+ public void processBlocking(Runnable runnable)
+ {
+ try
+ {
+ executor.submit(runnable).get();
+ }
+ catch (InterruptedException e)
+ {
+ throw new UncheckedInterruptedException(e);
+ }
+ catch (ExecutionException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public <T> Future<T> process(PreLoadContext loadCtx, Function<? super CommandStore, T> function)
+ {
+ AsyncOperation<T> operation = AsyncOperation.create(this, loadCtx, function);
+ executor.execute(operation);
+ return operation;
+ }
+
+ @Override
+ public Future<Void> process(PreLoadContext loadCtx, Consumer<? super CommandStore> consumer)
+ {
+ AsyncOperation<Void> operation = AsyncOperation.create(this, loadCtx, consumer);
+ executor.execute(operation);
+ return operation;
+ }
+
+ @Override
+ public void shutdown()
+ {
+ // executors are shutdown by AccordCommandStores
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordCommandStores.java b/src/java/org/apache/cassandra/service/accord/AccordCommandStores.java
new file mode 100644
index 0000000000..3a1cbb8064
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordCommandStores.java
@@ -0,0 +1,65 @@
+/*
+ * 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.concurrent.ExecutorService;
+
+import accord.api.Agent;
+import accord.api.DataStore;
+import accord.api.ProgressLog;
+import accord.local.CommandStore;
+import accord.local.CommandStores;
+import accord.local.Node;
+import org.apache.cassandra.concurrent.ExecutorFactory;
+import org.apache.cassandra.utils.ExecutorUtils;
+
+public class AccordCommandStores extends CommandStores
+{
+ private final ExecutorService[] executors;
+
+ public AccordCommandStores(int numShards, Node node, Agent agent, DataStore store,
+ ProgressLog.Factory progressLogFactory)
+ {
+ super(numShards, node, agent, store, progressLogFactory);
+ this.executors = new ExecutorService[numShards];
+ for (int i=0; i<numShards; i++)
+ {
+ executors[i] = ExecutorFactory.Global.executorFactory().sequential(CommandStore.class.getSimpleName() + '[' + node + ':' + i + ']');
+ }
+ }
+
+ @Override
+ protected CommandStore createCommandStore(int generation, int index, int numShards, Node node, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, CommandStore.RangesForEpoch rangesForEpoch)
+ {
+ return new AccordCommandStore(generation, index, numShards, node::uniqueNow, node.topology()::epoch, agent, store, progressLogFactory, rangesForEpoch, executors[index]);
+ }
+
+ void setCacheSize(long bytes)
+ {
+ setup(commandStore -> ((AccordCommandStore) commandStore).setCacheSize(bytes));
+ }
+
+ @Override
+ public synchronized void shutdown()
+ {
+ super.shutdown();
+ //TODO shutdown isn't useful by itself, we need a way to "wait" as well. Should be AutoCloseable or offer awaitTermination as well (think Shutdownable interface)
+ ExecutorUtils.shutdown(executors);
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordCommandsForKey.java b/src/java/org/apache/cassandra/service/accord/AccordCommandsForKey.java
new file mode 100644
index 0000000000..54ce46a3f9
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordCommandsForKey.java
@@ -0,0 +1,459 @@
+/*
+ * 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.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import com.google.common.base.Preconditions;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.local.Command;
+import accord.local.CommandStore;
+import accord.local.CommandsForKey;
+import accord.local.PartialCommand;
+import accord.local.Status;
+import accord.primitives.Timestamp;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.service.accord.store.StoredLong;
+import org.apache.cassandra.service.accord.store.StoredNavigableMap;
+import org.apache.cassandra.service.accord.store.StoredSet;
+import org.apache.cassandra.service.accord.store.StoredValue;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.concurrent.Future;
+
+import static org.apache.cassandra.service.accord.AccordState.WriteOnly.applyMapChanges;
+import static org.apache.cassandra.service.accord.AccordState.WriteOnly.applySetChanges;
+
+public class AccordCommandsForKey extends CommandsForKey implements AccordState<PartitionKey>
+{
+ private static final Logger logger = LoggerFactory.getLogger(AccordCommandsForKey.class);
+
+ private static final long EMPTY_SIZE = ObjectSizes.measureDeep(new AccordCommandsForKey(null, null));
+
+ public static class Defaults
+ {
+ public static final Timestamp maxTimestamp = Timestamp.NONE;
+ public static final Timestamp lastExecutedTimestamp = Timestamp.NONE;
+ public static final Timestamp lastWriteTimestamp = Timestamp.NONE;
+ public static final long lastExecutedMicros = 0;
+ }
+
+ public static class WriteOnly extends AccordCommandsForKey implements AccordState.WriteOnly<PartitionKey, AccordCommandsForKey>
+ {
+ private Future<?> future = null;
+
+ public WriteOnly(AccordCommandStore commandStore, PartitionKey key)
+ {
+ super(commandStore, key);
+ }
+
+ @Override
+ public void future(Future<?> future)
+ {
+ Preconditions.checkArgument(this.future == null);
+ this.future = future;
+
+ }
+
+ @Override
+ public Future<?> future()
+ {
+ return future;
+ }
+
+ @Override
+ public void applyChanges(AccordCommandsForKey instance)
+ {
+ applySetChanges(this, instance, cfk -> cfk.blindWitnessed);
+ applyMapChanges(this, instance, cfk -> cfk.uncommitted.map);
+ applyMapChanges(this, instance, cfk -> cfk.committedById.map);
+ applyMapChanges(this, instance, cfk -> cfk.committedByExecuteAt.map);
+ }
+ }
+
+ public enum SeriesKind
+ {
+ UNCOMMITTED(Command::txnId),
+ COMMITTED_BY_ID(Command::txnId),
+ COMMITTED_BY_EXECUTE_AT(Command::executeAt);
+
+ private final Function<Command, Timestamp> getTimestamp;
+
+ SeriesKind(Function<Command, Timestamp> timestampFunction)
+ {
+ this.getTimestamp = timestampFunction;
+ }
+ }
+
+ public class Series implements CommandTimeseries
+ {
+ public final SeriesKind kind;
+ public final StoredNavigableMap<Timestamp, ByteBuffer> map;
+
+ public Series(AccordState.Kind stateKind, SeriesKind kind)
+ {
+ this.kind = kind;
+ map = new StoredNavigableMap<>(stateKind);
+ }
+
+ @Override
+ public PartialCommand.WithDeps get(Timestamp timestamp)
+ {
+ ByteBuffer bytes = map.getView().get(timestamp);
+ if (bytes == null)
+ return null;
+ return AccordPartialCommand.WithDeps.serializer.deserialize(commandStore, bytes);
+ }
+
+ @Override
+ public void add(Timestamp timestamp, Command command)
+ {
+ map.blindPut(timestamp, AccordPartialCommand.WithDeps.serializer.serialize(command));
+ }
+
+ @Override
+ public void remove(Timestamp timestamp)
+ {
+ map.blindRemove(timestamp);
+ }
+
+ private Stream<PartialCommand.WithDeps> idsToCommands(Collection<ByteBuffer> blobs)
+ {
+ return blobs.stream().map(blob -> AccordPartialCommand.WithDeps.serializer.deserialize(commandStore, blob));
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ return map.getView().isEmpty();
+ }
+
+ @Override
+ public Stream<PartialCommand.WithDeps> before(Timestamp timestamp)
+ {
+ return idsToCommands(map.getView().headMap(timestamp, false).values());
+ }
+
+ @Override
+ public Stream<PartialCommand.WithDeps> after(Timestamp timestamp)
+ {
+ return idsToCommands(map.getView().tailMap(timestamp, false).values());
+ }
+
+ @Override
+ public Stream<PartialCommand.WithDeps> between(Timestamp min, Timestamp max)
+ {
+ return idsToCommands(map.getView().subMap(min, true, max, true).values());
+ }
+
+ @Override
+ public Stream<PartialCommand.WithDeps> all()
+ {
+ return idsToCommands(map.getView().values());
+ }
+ }
+
+ private final AccordCommandStore commandStore;
+ private final PartitionKey key;
+ public final StoredValue<Timestamp> maxTimestamp;
+ public final StoredValue<Timestamp> lastExecutedTimestamp;
+ public final StoredLong lastExecutedMicros;
+ public final StoredValue<Timestamp> lastWriteTimestamp;
+ public final StoredSet.Navigable<Timestamp> blindWitnessed;
+ public final Series uncommitted;
+ public final Series committedById;
+ public final Series committedByExecuteAt;
+
+ public AccordCommandsForKey(AccordCommandStore commandStore, PartitionKey key)
+ {
+ this.commandStore = commandStore;
+ this.key = key;
+ maxTimestamp = new StoredValue<>(kind());
+ lastExecutedTimestamp = new StoredValue<>(kind());
+ lastExecutedMicros = new StoredLong(kind());
+ lastWriteTimestamp = new StoredValue<>(kind());
+ blindWitnessed = new StoredSet.Navigable<>(kind());
+ uncommitted = new Series(kind(), SeriesKind.UNCOMMITTED);
+ committedById = new Series(kind(), SeriesKind.COMMITTED_BY_ID);
+ committedByExecuteAt = new Series(kind(), SeriesKind.COMMITTED_BY_EXECUTE_AT);
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ return maxTimestamp.isEmpty()
+ && lastExecutedTimestamp.isEmpty()
+ && lastExecutedMicros.isEmpty()
+ && lastWriteTimestamp.isEmpty()
+ && blindWitnessed.isEmpty()
+ && uncommitted.map.isEmpty()
+ && committedById.map.isEmpty()
+ && committedByExecuteAt.map.isEmpty();
+ }
+
+ public void setEmpty()
+ {
+ maxTimestamp.setEmpty();
+ lastExecutedTimestamp.setEmpty();
+ lastExecutedMicros.setEmpty();
+ lastWriteTimestamp.setEmpty();
+ blindWitnessed.setEmpty();
+ uncommitted.map.setEmpty();
+ committedById.map.setEmpty();
+ committedByExecuteAt.map.setEmpty();
+ }
+
+ public AccordCommandsForKey initialize()
+ {
+ maxTimestamp.set(Defaults.maxTimestamp);
+ lastExecutedTimestamp.load(Defaults.lastExecutedTimestamp);
+ lastExecutedMicros.load(Defaults.lastExecutedMicros);
+ lastWriteTimestamp.load(Defaults.lastWriteTimestamp);
+ blindWitnessed.load(new TreeSet<>());
+ uncommitted.map.load(new TreeMap<>());
+ committedById.map.load(new TreeMap<>());
+ committedByExecuteAt.map.load(new TreeMap<>());
+ return this;
+ }
+
+ @Override
+ public boolean hasModifications()
+ {
+ return maxTimestamp.hasModifications()
+ || lastExecutedTimestamp.hasModifications()
+ || lastExecutedMicros.hasModifications()
+ || lastWriteTimestamp.hasModifications()
+ || blindWitnessed.hasModifications()
+ || uncommitted.map.hasModifications()
+ || committedById.map.hasModifications()
+ || committedByExecuteAt.map.hasModifications();
+ }
+
+ @Override
+ public void clearModifiedFlag()
+ {
+ maxTimestamp.clearModifiedFlag();
+ lastExecutedTimestamp.clearModifiedFlag();
+ lastExecutedMicros.clearModifiedFlag();
+ lastWriteTimestamp.clearModifiedFlag();
+ blindWitnessed.clearModifiedFlag();
+ uncommitted.map.clearModifiedFlag();
+ committedById.map.clearModifiedFlag();
+ committedByExecuteAt.map.clearModifiedFlag();
+ }
+
+ @Override
+ public boolean isLoaded()
+ {
+ return maxTimestamp.isLoaded()
+ && lastExecutedTimestamp.isLoaded()
+ && lastExecutedMicros.isLoaded()
+ && lastWriteTimestamp.isLoaded()
+ && blindWitnessed.isLoaded()
+ && uncommitted.map.isLoaded()
+ && committedById.map.isLoaded()
+ && committedByExecuteAt.map.isLoaded();
+ }
+
+ public CommandStore commandStore()
+ {
+ return commandStore;
+ }
+
+ @Override
+ public PartitionKey key()
+ {
+ return key;
+ }
+
+ @Override
+ public long estimatedSizeOnHeap()
+ {
+ long size = EMPTY_SIZE;
+ size += maxTimestamp.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+ size += lastExecutedTimestamp.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+ size += lastExecutedMicros.estimatedSizeOnHeap();
+ size += lastWriteTimestamp.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+ size += blindWitnessed.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+ size += uncommitted.map.estimatedSizeOnHeap(AccordObjectSizes::timestamp, ByteBufferUtil::estimatedSizeOnHeap);
+ size += committedById.map.estimatedSizeOnHeap(AccordObjectSizes::timestamp, ByteBufferUtil::estimatedSizeOnHeap);
+ size += committedByExecuteAt.map.estimatedSizeOnHeap(AccordObjectSizes::timestamp, ByteBufferUtil::estimatedSizeOnHeap);
+ return size;
+ }
+
+ @Override
+ public CommandTimeseries uncommitted()
+ {
+ return uncommitted;
+ }
+
+ @Override
+ public CommandTimeseries committedById()
+ {
+ return committedById;
+ }
+
+ @Override
+ public CommandTimeseries committedByExecuteAt()
+ {
+ return committedByExecuteAt;
+ }
+
+ @Override
+ public Timestamp max()
+ {
+ return maxTimestamp.get();
+ }
+
+ @Override
+ public void updateMax(Timestamp timestamp)
+ {
+ if (isFullInstance())
+ {
+ if (maxTimestamp.get().compareTo(timestamp) >= 0)
+ return;
+ maxTimestamp.set(timestamp);
+ }
+ else
+ {
+ Preconditions.checkState(isWriteOnlyInstance());
+ blindWitnessed.blindAdd(timestamp);
+ }
+ }
+
+ public void applyBlindWitnessedTimestamps()
+ {
+ if (isEmpty() || blindWitnessed.getView().isEmpty())
+ return;
+
+ logger.trace("Applying blind witnessed timestamps for {}: {}", key(), blindWitnessed.getView());
+ blindWitnessed.getView().forEach(this::updateMax);
+ blindWitnessed.clear();
+ }
+
+ public void updateSummaries(AccordCommand command)
+ {
+ if (command.status.get().hasBeen(Status.Committed))
+ {
+ if (command.status.previous() == null || !command.status.previous().hasBeen(Status.Committed))
+ uncommitted.map.blindRemove(command.txnId());
+
+ ByteBuffer bb = AccordPartialCommand.WithDeps.serializer.serialize(command);
+ committedById.map.blindPut(command.txnId(), bb);
+ committedByExecuteAt.map.blindPut(command.executeAt(), bb);
+ }
+ else
+ {
+ uncommitted.map.blindPut(command.txnId(), AccordPartialCommand.WithDeps.serializer.serialize(command));
+ }
+ }
+
+ private static long getTimestampMicros(Timestamp timestamp)
+ {
+ return timestamp.real + timestamp.logical;
+ }
+
+ private void maybeUpdatelastTimestamp(Timestamp executeAt, boolean isForWriteTxn)
+ {
+ Timestamp lastWrite = lastWriteTimestamp.get();
+
+ if (executeAt.compareTo(lastWrite) < 0)
+ throw new IllegalArgumentException(String.format("%s is less than the most recent write timestamp %s", executeAt, lastWrite));
+
+ Timestamp lastExecuted = lastExecutedTimestamp.get();
+ int cmp = executeAt.compareTo(lastExecuted);
+ // execute can be in the past if it's for a read and after the most recent write
+ if (cmp == 0 || (!isForWriteTxn && cmp < 0))
+ return;
+ if (cmp < 0)
+ throw new IllegalArgumentException(String.format("%s is less than the most recent executed timestamp %s", executeAt, lastExecuted));
+
+ long micros = getTimestampMicros(executeAt);
+ long lastMicros = lastExecutedMicros.get();
+ lastExecutedTimestamp.set(executeAt);
+ lastExecutedMicros.set(Math.max(micros, lastMicros + 1));
+ if (isForWriteTxn)
+ lastWriteTimestamp.set(executeAt);
+ }
+
+ public int nowInSecondsFor(Timestamp executeAt, boolean isForWriteTxn)
+ {
+ maybeUpdatelastTimestamp(executeAt, isForWriteTxn);
+ // we use the executeAt time instead of the monotonic database timestamp to prevent uneven
+ // ttl expiration in extreme cases, ie 1M+ writes/second to a key causing timestamps to overflow
+ // into the next second on some keys and not others.
+ return (int) TimeUnit.MICROSECONDS.toSeconds(getTimestampMicros(lastExecutedTimestamp.get()));
+ }
+
+ public long timestampMicrosFor(Timestamp executeAt, boolean isForWriteTxn)
+ {
+ maybeUpdatelastTimestamp(executeAt, isForWriteTxn);
+ return lastExecutedMicros.get();
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AccordCommandsForKey that = (AccordCommandsForKey) o;
+ return commandStore == that.commandStore
+ && key.equals(that.key)
+ && maxTimestamp.equals(that.maxTimestamp)
+ && lastExecutedTimestamp.equals(that.lastExecutedTimestamp)
+ && lastExecutedMicros.equals(that.lastExecutedMicros)
+ && lastWriteTimestamp.equals(that.lastWriteTimestamp)
+ && blindWitnessed.equals(that.blindWitnessed)
+ && uncommitted.map.equals(that.uncommitted.map)
+ && committedById.map.equals(that.committedById.map)
+ && committedByExecuteAt.map.equals(that.committedByExecuteAt.map);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(commandStore, key, blindWitnessed, maxTimestamp, lastExecutedTimestamp, lastExecutedMicros, lastWriteTimestamp, uncommitted, committedById, committedByExecuteAt);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "AccordCommandsForKey{" +
+ "key=" + key +
+ ", maxTs=" + maxTimestamp +
+ ", lastExecutedTimestamp=" + lastExecutedTimestamp +
+ ", lastExecutedMicros=" + lastExecutedMicros +
+ ", lastWriteTimestamp=" + lastWriteTimestamp +
+// ", blindWitnessed=" + blindWitnessed +
+// ", uncommitted=" + uncommitted.map +
+// ", committedById=" + committedById.map +
+// ", committedByExecuteAt=" + committedByExecuteAt.map +
+ '}';
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordConfigurationService.java b/src/java/org/apache/cassandra/service/accord/AccordConfigurationService.java
new file mode 100644
index 0000000000..784af390eb
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordConfigurationService.java
@@ -0,0 +1,88 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+
+import accord.api.ConfigurationService;
+import accord.local.Node;
+import accord.topology.Topology;
+import org.apache.cassandra.utils.concurrent.Future;
+
+/**
+ * Currently a stubbed out config service meant to be triggered from a dtest
+ */
+public class AccordConfigurationService implements ConfigurationService
+{
+ private final Node.Id localId;
+ private final List<Listener> listeners = new ArrayList<>();
+ private final List<Topology> epochs = new ArrayList<>();
+
+ public AccordConfigurationService(Node.Id localId)
+ {
+ this.localId = localId;
+ epochs.add(Topology.EMPTY);
+ }
+
+ @Override
+ public synchronized void registerListener(Listener listener)
+ {
+ listeners.add(listener);
+ }
+
+ @Override
+ public synchronized Topology currentTopology()
+ {
+ return epochs.get(epochs.size() - 1);
+ }
+
+ @Override
+ public Topology getTopologyForEpoch(long epoch)
+ {
+ return epochs.get((int) epoch);
+ }
+
+ @Override
+ public void fetchTopologyForEpoch(long epoch)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void acknowledgeEpoch(long epoch)
+ {
+ Topology acknowledged = getTopologyForEpoch(epoch);
+ for (Node.Id node : acknowledged.nodes())
+ {
+ if (node.equals(localId))
+ continue;
+ for (Listener listener : listeners)
+ listener.onEpochSyncComplete(node, epoch);
+ }
+ }
+
+ public void createEpochFromConfig()
+ {
+ Topology topology = AccordTopologyUtils.createTopology(epochs.size());
+ epochs.add(topology);
+ for (Listener listener : listeners)
+ listener.onTopologyUpdate(topology);
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordKeyspace.java b/src/java/org/apache/cassandra/service/accord/AccordKeyspace.java
new file mode 100644
index 0000000000..d2c3ef6928
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordKeyspace.java
@@ -0,0 +1,843 @@
+/*
+ * 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.EnumMap;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.NavigableSet;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.local.CommandStore;
+import accord.local.Node;
+import accord.local.Status;
+import accord.primitives.Ballot;
+import accord.primitives.Deps;
+import accord.primitives.Timestamp;
+import accord.primitives.TxnId;
+import accord.utils.DeterministicIdentitySet;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.cql3.statements.schema.CreateTableStatement;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.ClusteringComparator;
+import org.apache.cassandra.db.Columns;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.DeletionTime;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.ReadExecutionController;
+import org.apache.cassandra.db.RegularAndStaticColumns;
+import org.apache.cassandra.db.SinglePartitionReadCommand;
+import org.apache.cassandra.db.Slices;
+import org.apache.cassandra.db.filter.ClusteringIndexFilter;
+import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.filter.DataLimits;
+import org.apache.cassandra.db.filter.RowFilter;
+import org.apache.cassandra.db.marshal.ByteBufferAccessor;
+import org.apache.cassandra.db.marshal.BytesType;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.db.marshal.TupleType;
+import org.apache.cassandra.db.marshal.ValueAccessor;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.rows.BTreeRow;
+import org.apache.cassandra.db.rows.Cell;
+import org.apache.cassandra.db.rows.CellPath;
+import org.apache.cassandra.db.rows.ComplexColumnData;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.rows.RowIterator;
+import org.apache.cassandra.db.transform.FilteredPartitions;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.Functions;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.schema.Tables;
+import org.apache.cassandra.schema.Types;
+import org.apache.cassandra.schema.Views;
+import org.apache.cassandra.serializers.UUIDSerializer;
+import org.apache.cassandra.service.accord.AccordCommandsForKey.SeriesKind;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.service.accord.db.AccordData;
+import org.apache.cassandra.service.accord.serializers.CommandSerializers;
+import org.apache.cassandra.service.accord.store.StoredNavigableMap;
+import org.apache.cassandra.service.accord.store.StoredSet;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.Clock;
+
+import static java.lang.String.format;
+import static org.apache.cassandra.cql3.QueryProcessor.executeOnceInternal;
+import static org.apache.cassandra.db.rows.BufferCell.*;
+import static org.apache.cassandra.schema.SchemaConstants.ACCORD_KEYSPACE_NAME;
+import static org.apache.cassandra.utils.ByteBufferUtil.EMPTY_BYTE_BUFFER;
+import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
+
+public class AccordKeyspace
+{
+ private static final Logger logger = LoggerFactory.getLogger(AccordKeyspace.class);
+
+ public static final String COMMANDS = "commands";
+ public static final String COMMAND_SERIES = "command_series";
+ public static final String COMMANDS_FOR_KEY = "commands_for_key";
+
+ private static final String TIMESTAMP_TUPLE = "tuple<bigint, bigint, int, bigint>";
+ private static final TupleType TIMESTAMP_TYPE = new TupleType(Lists.newArrayList(LongType.instance, LongType.instance, Int32Type.instance, LongType.instance));
+ private static final String KEY_TUPLE = "tuple<uuid, blob>";
+
+ private static final ClusteringIndexFilter FULL_PARTITION = new ClusteringIndexSliceFilter(Slices.ALL, false);
+
+ // TODO: store timestamps as blobs (confirm there are no negative numbers, or offset)
+ private static final TableMetadata Commands =
+ parse(COMMANDS,
+ "accord commands",
+ "CREATE TABLE %s ("
+ + "store_generation int,"
+ + "store_index int,"
+ + format("txn_id %s,", TIMESTAMP_TUPLE)
+ + "status int,"
+ + "home_key blob,"
+ + "home_key_version int,"
+ + "progress_key blob,"
+ + "progress_key_version blob,"
+ + "is_globally_persistent boolean,"
+ + "txn_version int,"
+ + "txn blob,"
+ + format("execute_at %s,", TIMESTAMP_TUPLE)
+ + format("promised_ballot %s,", TIMESTAMP_TUPLE)
+ + format("accepted_ballot %s,", TIMESTAMP_TUPLE)
+ + "dependencies_version int,"
+ + "dependencies blob,"
+ + "writes_version int,"
+ + "writes blob,"
+ + "result_version int,"
+ + "result blob,"
+ + format("waiting_on_commit map<%s, blob>,", TIMESTAMP_TUPLE)
+ + format("waiting_on_apply map<%s, blob>,", TIMESTAMP_TUPLE)
+ + "listeners set<blob>, "
+ + format("blocking_commit_on set<%s>, ", TIMESTAMP_TUPLE)
+ + format("blocking_apply_on set<%s>, ", TIMESTAMP_TUPLE)
+ + "PRIMARY KEY((store_generation, store_index, txn_id))"
+ + ')');
+
+ private static ColumnMetadata getColumn(TableMetadata metadata, String name)
+ {
+ ColumnMetadata column = metadata.getColumn(new ColumnIdentifier(name, true));
+ if (column == null)
+ throw new IllegalArgumentException(String.format("Unknown column %s for %s.%s", name, metadata.keyspace, metadata.name));
+ return column;
+ }
+
+ private static class CommandsColumns
+ {
+ static final ClusteringComparator keyComparator = Commands.partitionKeyAsClusteringComparator();
+ static final ColumnMetadata status = getColumn(Commands, "status");
+ static final ColumnMetadata home_key = getColumn(Commands, "home_key");
+ static final ColumnMetadata home_key_version = getColumn(Commands, "home_key_version");
+ static final ColumnMetadata progress_key = getColumn(Commands, "progress_key");
+ static final ColumnMetadata progress_key_version = getColumn(Commands, "progress_key_version");
+ static final ColumnMetadata is_globally_persistent = getColumn(Commands, "is_globally_persistent");
+ static final ColumnMetadata txn_version = getColumn(Commands, "txn_version");
+ static final ColumnMetadata txn = getColumn(Commands, "txn");
+ 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 dependencies_version = getColumn(Commands, "dependencies_version");
+ static final ColumnMetadata dependencies = getColumn(Commands, "dependencies");
+ static final ColumnMetadata writes_version = getColumn(Commands, "writes_version");
+ static final ColumnMetadata writes = getColumn(Commands, "writes");
+ static final ColumnMetadata result_version = getColumn(Commands, "result_version");
+ static final ColumnMetadata result = getColumn(Commands, "result");
+ static final ColumnMetadata waiting_on_commit = getColumn(Commands, "waiting_on_commit");
+ static final ColumnMetadata waiting_on_apply = getColumn(Commands, "waiting_on_apply");
+ static final ColumnMetadata listeners = getColumn(Commands, "listeners");
+ static final ColumnMetadata blocking_commit_on = getColumn(Commands, "blocking_commit_on");
+ static final ColumnMetadata blocking_apply_on = getColumn(Commands, "blocking_apply_on");
+ }
+
+ private static final TableMetadata CommandsForKey =
+ parse(COMMANDS_FOR_KEY,
+ "accord commands per key",
+ "CREATE TABLE %s ("
+ + "store_generation int, "
+ + "store_index int, "
+ + format("key %s, ", KEY_TUPLE)
+ + format("max_timestamp %s static, ", TIMESTAMP_TUPLE)
+ + format("last_executed_timestamp %s static, ", TIMESTAMP_TUPLE)
+ + format("last_executed_micros bigint static, ")
+ + format("last_write_timestamp %s static, ", TIMESTAMP_TUPLE)
+ + format("blind_witnessed set<%s> static, ", TIMESTAMP_TUPLE)
+ + "series int, "
+ + format("timestamp %s, ", TIMESTAMP_TUPLE)
+ + "data blob, "
+ + "PRIMARY KEY((store_generation, store_index, key), series, timestamp)"
+ + ')');
+
+ private static class CommandsForKeyColumns
+ {
+ static final ClusteringComparator keyComparator = CommandsForKey.partitionKeyAsClusteringComparator();
+ static final ColumnFilter allColumns = ColumnFilter.all(CommandsForKey);
+ static final ColumnMetadata max_timestamp = getColumn(CommandsForKey, "max_timestamp");
+ static final ColumnMetadata last_executed_timestamp = getColumn(CommandsForKey, "last_executed_timestamp");
+ static final ColumnMetadata last_executed_micros = getColumn(CommandsForKey, "last_executed_micros");
+ static final ColumnMetadata last_write_timestamp = getColumn(CommandsForKey, "last_write_timestamp");
+ static final ColumnMetadata blind_witnessed = getColumn(CommandsForKey, "blind_witnessed");
+
+ static final ColumnMetadata series = getColumn(CommandsForKey, "series");
+ static final ColumnMetadata timestamp = getColumn(CommandsForKey, "timestamp");
+ static final ColumnMetadata data = getColumn(CommandsForKey, "data");
+
+ static final Columns statics = Columns.from(Lists.newArrayList(max_timestamp, last_executed_timestamp, last_executed_micros, last_write_timestamp, blind_witnessed));
+ static final Columns regulars = Columns.from(Lists.newArrayList(data));
+ private static final RegularAndStaticColumns all = new RegularAndStaticColumns(statics, regulars);
+ private static final RegularAndStaticColumns justStatic = new RegularAndStaticColumns(statics, Columns.NONE);
+ private static final RegularAndStaticColumns justRegular = new RegularAndStaticColumns(Columns.NONE, regulars);
+
+ static boolean hasStaticChanges(AccordCommandsForKey commandsForKey)
+ {
+ return commandsForKey.maxTimestamp.hasModifications()
+ || commandsForKey.lastExecutedTimestamp.hasModifications()
+ || commandsForKey.lastExecutedMicros.hasModifications()
+ || commandsForKey.blindWitnessed.hasModifications();
+ }
+
+ static RegularAndStaticColumns columnsFor(AccordCommandsForKey commandsForKey)
+ {
+ boolean hasStaticChanges = hasStaticChanges(commandsForKey);
+ boolean hasRegularChanges = commandsForKey.uncommitted.map.hasAdditions()
+ || commandsForKey.committedById.map.hasAdditions()
+ || commandsForKey.committedByExecuteAt.map.hasAdditions();
+
+ if (hasStaticChanges && hasRegularChanges)
+ return all;
+ else if (hasStaticChanges)
+ return justStatic;
+ else if (hasRegularChanges)
+ return justRegular;
+ else
+ throw new IllegalArgumentException();
+ }
+ }
+
+ private static TableMetadata parse(String name, String description, String cql)
+ {
+ return CreateTableStatement.parse(format(cql, name), ACCORD_KEYSPACE_NAME)
+ .id(TableId.forSystemTable(ACCORD_KEYSPACE_NAME, name))
+ .comment(description)
+ .gcGraceSeconds((int) TimeUnit.DAYS.toSeconds(90))
+ .build();
+ }
+
+ public static KeyspaceMetadata metadata()
+ {
+ return KeyspaceMetadata.create(ACCORD_KEYSPACE_NAME, KeyspaceParams.local(), tables(), Views.none(), Types.none(), Functions.none());
+ }
+
+ private static Tables tables()
+ {
+ return Tables.of(Commands, CommandsForKey);
+ }
+
+ private static <T> ByteBuffer serialize(T obj, IVersionedSerializer<T> serializer, int version) throws IOException
+ {
+ int size = (int) serializer.serializedSize(obj, version);
+ try (DataOutputBuffer out = new DataOutputBuffer(size))
+ {
+ serializer.serialize(obj, out, version);
+ assert size == out.buffer().limit();
+ return out.buffer();
+ }
+ }
+
+ private static <T> ByteBuffer serializeOrNull(T obj, IVersionedSerializer<T> serializer, int version) throws IOException
+ {
+ return obj != null ? serialize(obj, serializer, version) : EMPTY_BYTE_BUFFER;
+ }
+
+ private static <T> T deserialize(ByteBuffer bytes, IVersionedSerializer<T> serializer, int version) throws IOException
+ {
+ try (DataInputBuffer in = new DataInputBuffer(bytes, true))
+ {
+ return serializer.deserialize(in, version);
+ }
+ }
+
+ private static <T> T deserializeOrNull(ByteBuffer bytes, IVersionedSerializer<T> serializer, int version) throws IOException
+ {
+ return bytes != null && ! ByteBufferAccessor.instance.isEmpty(bytes) ? deserialize(bytes, serializer, version) : null;
+ }
+
+ private static Map<ByteBuffer, ByteBuffer> serializeWaitingOn(Map<TxnId, ByteBuffer> waitingOn)
+ {
+ Map<ByteBuffer, ByteBuffer> result = Maps.newHashMapWithExpectedSize(waitingOn.size());
+ for (Map.Entry<TxnId, ByteBuffer> entry : waitingOn.entrySet())
+ result.put(serializeTimestamp(entry.getKey()), entry.getValue());
+ return result;
+ }
+
+ private static NavigableMap<TxnId, ByteBuffer> deserializeWaitingOn(Map<ByteBuffer, ByteBuffer> serialized)
+ {
+ if (serialized == null || serialized.isEmpty())
+ return new TreeMap<>();
+
+ NavigableMap<TxnId, ByteBuffer> result = new TreeMap<>();
+ for (Map.Entry<ByteBuffer, ByteBuffer> entry : serialized.entrySet())
+ result.put(deserializeTimestampOrNull(entry.getKey(), TxnId::new), entry.getValue());
+ return result;
+ }
+
+ private static NavigableMap<TxnId, ByteBuffer> deserializeWaitingOn(UntypedResultSet.Row row, String name)
+ {
+ return deserializeWaitingOn(row.getMap(name, BytesType.instance, BytesType.instance));
+ }
+
+ private static <T extends Timestamp, S extends Set<T>> S deserializeTimestampSet(Set<ByteBuffer> serialized, Supplier<S> setFactory, TimestampFactory<T> timestampFactory)
+ {
+ S result = setFactory.get();
+ if (serialized == null || serialized.isEmpty())
+ return result;
+
+ for (ByteBuffer bytes : serialized)
+ result.add(deserializeTimestampOrNull(bytes, timestampFactory));
+
+ return result;
+ }
+
+ private static NavigableSet<TxnId> deserializeBlocking(UntypedResultSet.Row row, String name)
+ {
+ return deserializeTimestampSet(row.getSet(name, BytesType.instance), TreeSet::new, TxnId::new);
+ }
+
+ public static Set<ByteBuffer> serializeListeners(Set<ListenerProxy> listeners)
+ {
+ Set<ByteBuffer> result = Sets.newHashSetWithExpectedSize(listeners.size());
+ for (ListenerProxy listener : listeners)
+ {
+ result.add(listener.identifier());
+ }
+ return result;
+ }
+
+ private static DeterministicIdentitySet<ListenerProxy> deserializeListeners(CommandStore commandStore, Set<ByteBuffer> serialized) throws IOException
+ {
+ if (serialized == null || serialized.isEmpty())
+ return new DeterministicIdentitySet<>();
+ DeterministicIdentitySet<ListenerProxy> result = new DeterministicIdentitySet<>();
+ for (ByteBuffer bytes : serialized)
+ {
+ result.add(ListenerProxy.deserialize(commandStore, bytes, ByteBufferAccessor.instance, 0));
+ }
+ return result;
+ }
+
+ private static DeterministicIdentitySet<ListenerProxy> deserializeListeners(CommandStore commandStore, UntypedResultSet.Row row, String name) throws IOException
+ {
+ return deserializeListeners(commandStore, row.getSet(name, BytesType.instance));
+ }
+
+ private static <K extends Comparable<?>, V> void addStoredMapChanges(Row.Builder builder,
+ ColumnMetadata column,
+ long timestamp,
+ int nowInSec,
+ StoredNavigableMap<K, V> map,
+ Function<K, ByteBuffer> serializeKey,
+ Function<V, ByteBuffer> serializeVal)
+ {
+ if (map.wasCleared())
+ {
+ if (!map.hasAdditions())
+ {
+ builder.addComplexDeletion(column, new DeletionTime(timestamp, nowInSec));
+ return;
+ }
+ else
+ builder.addComplexDeletion(column, new DeletionTime(timestamp - 1, nowInSec));
+ }
+
+ map.forEachAddition((k, v) -> builder.addCell(live(column, timestamp, serializeVal.apply(v), CellPath.create(serializeKey.apply(k)))));
+
+ if (!map.wasCleared())
+ map.forEachDeletion(k -> builder.addCell(tombstone(column, timestamp, nowInSec, CellPath.create(serializeKey.apply(k)))));
+ }
+
+ private static <T extends Comparable<?>> void addStoredSetChanges(Row.Builder builder,
+ ColumnMetadata column,
+ long timestamp,
+ int nowInSec,
+ StoredSet<T, ?> map,
+ Function<T, ByteBuffer> serialize)
+ {
+ if (map.wasCleared())
+ {
+ if (!map.hasAdditions())
+ {
+ builder.addComplexDeletion(column, new DeletionTime(timestamp, nowInSec));
+ return;
+ }
+ else
+ builder.addComplexDeletion(column, new DeletionTime(timestamp - 1, nowInSec));
+ }
+
+ map.forEachAddition(i -> builder.addCell(live(column, timestamp, EMPTY_BYTE_BUFFER, CellPath.create(serialize.apply(i)))));
+
+ if (!map.wasCleared())
+ map.forEachDeletion(k -> builder.addCell(tombstone(column, timestamp, nowInSec, CellPath.create(serialize.apply(k)))));
+ }
+
+ public static Mutation getCommandMutation(AccordCommand command, long timestampMicros)
+ {
+ try
+ {
+ Preconditions.checkArgument(command.hasModifications());
+
+ // TODO: convert to byte arrays
+ ValueAccessor<ByteBuffer> accessor = ByteBufferAccessor.instance;
+
+ Row.Builder builder = BTreeRow.unsortedBuilder();
+ builder.newRow(Clustering.EMPTY);
+ int nowInSeconds = (int) TimeUnit.MICROSECONDS.toSeconds(timestampMicros);
+ int version = MessagingService.current_version;
+ ByteBuffer versionBytes = accessor.valueOf(version);
+
+ if (command.status.hasModifications())
+ builder.addCell(live(CommandsColumns.status, timestampMicros, accessor.valueOf(command.status.get().ordinal())));
+
+ if (command.homeKey.hasModifications())
+ {
+ builder.addCell(live(CommandsColumns.home_key_version, timestampMicros, versionBytes));
+ builder.addCell(live(CommandsColumns.home_key, timestampMicros, serializeOrNull((AccordKey) command.homeKey.get(), AccordKey.serializer, version)));
+ }
+
+ if (command.progressKey.hasModifications())
+ {
+ builder.addCell(live(CommandsColumns.progress_key_version, timestampMicros, versionBytes));
+ builder.addCell(live(CommandsColumns.progress_key, timestampMicros, serializeOrNull((AccordKey) command.progressKey.get(), AccordKey.serializer, version)));
+ }
+
+ if (command.isGloballyPersistent.hasModifications())
+ builder.addCell(live(CommandsColumns.is_globally_persistent, timestampMicros, accessor.valueOf(command.isGloballyPersistent.get())));
+
+ if (command.txn.hasModifications())
+ {
+ builder.addCell(live(CommandsColumns.txn_version, timestampMicros, versionBytes));
+ builder.addCell(live(CommandsColumns.txn, timestampMicros, serializeOrNull(command.txn.get(), CommandSerializers.txn, version)));
+ }
+
+ if (command.executeAt.hasModifications())
+ builder.addCell(live(CommandsColumns.execute_at, timestampMicros, serializeTimestamp(command.executeAt.get())));
+
+ if (command.promised.hasModifications())
+ builder.addCell(live(CommandsColumns.promised_ballot, timestampMicros, serializeTimestamp(command.promised.get())));
+
+ if (command.accepted.hasModifications())
+ builder.addCell(live(CommandsColumns.accepted_ballot, timestampMicros, serializeTimestamp(command.accepted.get())));
+
+ if (command.deps.hasModifications())
+ {
+ builder.addCell(live(CommandsColumns.dependencies_version, timestampMicros, versionBytes));
+ builder.addCell(live(CommandsColumns.dependencies, timestampMicros, serialize(command.deps.get(), CommandSerializers.deps, version)));
+ }
+
+ if (command.writes.hasModifications())
+ {
+ builder.addCell(live(CommandsColumns.writes_version, timestampMicros, versionBytes));
+ builder.addCell(live(CommandsColumns.writes, timestampMicros, serialize(command.writes.get(), CommandSerializers.writes, version)));
+ }
+
+ if (command.result.hasModifications())
+ {
+ builder.addCell(live(CommandsColumns.result_version, timestampMicros, versionBytes));
+ builder.addCell(live(CommandsColumns.result, timestampMicros, serialize((AccordData) command.result.get(), AccordData.serializer, version)));
+ }
+
+ if (command.waitingOnCommit.hasModifications())
+ {
+ addStoredMapChanges(builder, CommandsColumns.waiting_on_commit,
+ timestampMicros, nowInSeconds, command.waitingOnCommit,
+ AccordKeyspace::serializeTimestamp, bytes -> bytes);
+ }
+
+ if (command.blockingCommitOn.hasModifications())
+ {
+ addStoredSetChanges(builder, CommandsColumns.blocking_commit_on,
+ timestampMicros, nowInSeconds, command.blockingApplyOn,
+ AccordKeyspace::serializeTimestamp);
+ }
+
+ if (command.waitingOnApply.hasModifications())
+ {
+ addStoredMapChanges(builder, CommandsColumns.waiting_on_apply,
+ timestampMicros, nowInSeconds, command.waitingOnApply,
+ AccordKeyspace::serializeTimestamp, bytes -> bytes);
+ }
+
+ if (command.blockingApplyOn.hasModifications())
+ {
+ addStoredSetChanges(builder, CommandsColumns.blocking_apply_on,
+ timestampMicros, nowInSeconds, command.blockingApplyOn,
+ AccordKeyspace::serializeTimestamp);
+ }
+
+ if (command.storedListeners.hasModifications())
+ {
+ addStoredSetChanges(builder, CommandsColumns.listeners,
+ timestampMicros, nowInSeconds, command.storedListeners,
+ ListenerProxy::identifier);
+ }
+ ByteBuffer key = CommandsColumns.keyComparator.make(command.commandStore().generation(),
+ command.commandStore().index(),
+ serializeTimestamp(command.txnId())).serializeAsPartitionKey();
+ PartitionUpdate update = PartitionUpdate.singleRowUpdate(Commands, key, builder.build());
+ return new Mutation(update);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static ByteBuffer serializeKey(PartitionKey key)
+ {
+ UUIDSerializer.instance.serialize(key.tableId().asUUID());
+ return TupleType.buildValue(new ByteBuffer[]{UUIDSerializer.instance.serialize(key.tableId().asUUID()),
+ key.partitionKey().getKey()});
+ }
+
+ private static ByteBuffer serializeTimestamp(Timestamp timestamp)
+ {
+ return TupleType.buildValue(new ByteBuffer[]{bytes(timestamp.epoch), bytes(timestamp.real), bytes(timestamp.logical), bytes(timestamp.node.id)});
+ }
+
+ public interface TimestampFactory<T extends Timestamp>
+ {
+ T create(long epoch, long real, int logical, Node.Id node);
+ }
+
+ public static <T extends Timestamp> T deserializeTimestampOrNull(ByteBuffer bytes, TimestampFactory<T> factory)
+ {
+ if (bytes == null || ByteBufferAccessor.instance.isEmpty(bytes))
+ return null;
+ ByteBuffer[] split = TIMESTAMP_TYPE.split(ByteBufferAccessor.instance, bytes);
+ return factory.create(split[0].getLong(), split[1].getLong(), split[2].getInt(), new Node.Id(split[3].getLong()));
+ }
+
+ private static <T extends Timestamp> T deserializeTimestampOrNull(UntypedResultSet.Row row, String name, TimestampFactory<T> factory)
+ {
+ return deserializeTimestampOrNull(row.getBlob(name), factory);
+ }
+
+ public static AccordCommand loadCommand(AccordCommandStore commandStore, TxnId txnId)
+ {
+ AccordCommand command = new AccordCommand(commandStore, txnId);
+ loadCommand(command);
+ return command;
+ }
+
+ private static <T> T deserializeWithVersionOr(UntypedResultSet.Row row, String dataColumn, String versionColumn, IVersionedSerializer<T> serializer, Supplier<T> defaultSupplier) throws IOException
+ {
+ if (!row.has(versionColumn))
+ return defaultSupplier.get();
+
+ return deserialize(row.getBlob(dataColumn), serializer, row.getInt(versionColumn));
+ }
+
+ public static UntypedResultSet loadCommandRow(CommandStore commandStore, TxnId txnId)
+ {
+ String cql = "SELECT * FROM %s.%s " +
+ "WHERE store_generation=? " +
+ "AND store_index=? " +
+ "AND txn_id=(?, ?, ?, ?)";
+
+ return executeOnceInternal(String.format(cql, ACCORD_KEYSPACE_NAME, COMMANDS),
+ commandStore.generation(),
+ commandStore.index(),
+ txnId.epoch, txnId.real, txnId.logical, txnId.node.id);
+ }
+
+ public static void loadCommand(AccordCommand command)
+ {
+ Preconditions.checkArgument(!command.isLoaded());
+ TxnId txnId = command.txnId();
+ AccordCommandStore commandStore = command.commandStore();
+ commandStore.checkNotInStoreThread();
+
+ String cql = "SELECT * FROM %s.%s " +
+ "WHERE store_generation=? " +
+ "AND store_index=? " +
+ "AND txn_id=(?, ?, ?, ?)";
+
+ UntypedResultSet result = loadCommandRow(commandStore, command.txnId());
+
+ if (result.isEmpty())
+ {
+ command.setEmpty();
+ return;
+ }
+
+ try
+ {
+ UntypedResultSet.Row row = result.one();
+ Preconditions.checkState(deserializeTimestampOrNull(row, "txn_id", TxnId::new).equals(txnId));
+ command.status.load(Status.values()[row.getInt("status")]);
+ command.homeKey.load(deserializeOrNull(row.getBlob("home_key"), AccordKey.serializer, row.getInt("home_key_version")));
+ command.progressKey.load(deserializeOrNull(row.getBlob("progress_key"), AccordKey.serializer, row.getInt("progress_key_version")));
+ command.isGloballyPersistent.load(row.getBoolean("is_globally_persistent"));
+ command.txn.load(deserializeOrNull(row.getBlob("txn"), CommandSerializers.txn, row.getInt("txn_version")));
+ command.executeAt.load(deserializeTimestampOrNull(row, "execute_at", Timestamp::new));
+ command.promised.load(deserializeTimestampOrNull(row, "promised_ballot", Ballot::new));
+ command.accepted.load(deserializeTimestampOrNull(row, "accepted_ballot", Ballot::new));
+ command.deps.load(deserializeWithVersionOr(row, "dependencies", "dependencies_version", CommandSerializers.deps, () -> Deps.NONE));
+ command.writes.load(deserializeWithVersionOr(row, "writes", "writes_version", CommandSerializers.writes, () -> null));
+ command.result.load(deserializeWithVersionOr(row, "result", "result_version", AccordData.serializer, () -> null));
+ command.waitingOnCommit.load(deserializeWaitingOn(row, "waiting_on_commit"));
+ command.blockingCommitOn.load(deserializeBlocking(row, "blocking_commit_on"));
+ command.waitingOnApply.load(deserializeWaitingOn(row, "waiting_on_apply"));
+ command.blockingApplyOn.load(deserializeBlocking(row, "blocking_apply_on"));
+ command.storedListeners.load(deserializeListeners(commandStore, row, "listeners"));
+ }
+ catch (IOException e)
+ {
+ logger.error("Exception loading AccordCommand " + command.txnId(), e);
+ throw new RuntimeException(e);
+ }
+ catch (Throwable t)
+ {
+ logger.error("Exception loading AccordCommand " + command.txnId(), t);
+ throw t;
+ }
+ }
+
+ private static void addSeriesMutations(AccordCommandsForKey.Series series,
+ PartitionUpdate.Builder partitionBuilder,
+ Row.Builder rowBuilder,
+ long timestampMicros,
+ int nowInSeconds)
+ {
+ if (!series.map.hasModifications())
+ return;
+
+ Row.Deletion deletion = series.map.hasDeletions() ?
+ Row.Deletion.regular(new DeletionTime(timestampMicros, nowInSeconds)) :
+ null;
+ ByteBuffer ordinalBytes = bytes(series.kind.ordinal());
+ series.map.forEachAddition((timestamp, bytes) -> {
+ rowBuilder.newRow(Clustering.make(ordinalBytes, serializeTimestamp(timestamp)));
+ rowBuilder.addCell(live(CommandsForKeyColumns.data, timestampMicros, bytes));
+ partitionBuilder.add(rowBuilder.build());
+ });
+ series.map.forEachDeletion(timestamp -> {
+ rowBuilder.newRow(Clustering.make(ordinalBytes, serializeTimestamp(timestamp)));
+ rowBuilder.addRowDeletion(deletion);
+ partitionBuilder.add(rowBuilder.build());
+ });
+ }
+
+ private static DecoratedKey makeKey(CommandStore commandStore, PartitionKey key)
+ {
+ ByteBuffer pk = CommandsForKeyColumns.keyComparator.make(commandStore.generation(),
+ commandStore.index(),
+ serializeKey(key)).serializeAsPartitionKey();
+ return CommandsForKey.partitioner.decorateKey(pk);
+ }
+
+ private static DecoratedKey makeKey(AccordCommandsForKey cfk)
+ {
+ return makeKey(cfk.commandStore(), cfk.key());
+ }
+
+ public static Mutation getCommandsForKeyMutation(AccordCommandsForKey cfk, long timestampMicros)
+ {
+ Preconditions.checkArgument(cfk.hasModifications());
+
+ int nowInSeconds = (int) TimeUnit.MICROSECONDS.toSeconds(timestampMicros);
+
+ int expectedRows = (CommandsForKeyColumns.hasStaticChanges(cfk) ? 1 : 0)
+ + cfk.uncommitted.map.totalModifications()
+ + cfk.committedById.map.totalModifications()
+ + cfk.committedByExecuteAt.map.totalModifications();
+
+ PartitionUpdate.Builder partitionBuilder = new PartitionUpdate.Builder(CommandsForKey,
+ makeKey(cfk),
+ CommandsForKeyColumns.columnsFor(cfk),
+ expectedRows);
+
+ Row.Builder rowBuilder = BTreeRow.unsortedBuilder();
+ boolean updateStaticRow = cfk.maxTimestamp.hasModifications()
+ || cfk.lastExecutedTimestamp.hasModifications()
+ || cfk.lastExecutedMicros.hasModifications()
+ || cfk.blindWitnessed.hasModifications();
+ if (updateStaticRow)
+ rowBuilder.newRow(Clustering.STATIC_CLUSTERING);
+
+ if (cfk.maxTimestamp.hasModifications())
+ rowBuilder.addCell(live(CommandsForKeyColumns.max_timestamp, timestampMicros, serializeTimestamp(cfk.maxTimestamp.get())));
+
+ if (cfk.lastExecutedTimestamp.hasModifications())
+ rowBuilder.addCell(live(CommandsForKeyColumns.last_executed_timestamp, timestampMicros, serializeTimestamp(cfk.lastExecutedTimestamp.get())));
+
+ if (cfk.lastExecutedMicros.hasModifications())
+ rowBuilder.addCell(live(CommandsForKeyColumns.last_executed_micros, timestampMicros, ByteBufferUtil.bytes(cfk.lastExecutedMicros.get())));
+
+ if (cfk.lastWriteTimestamp.hasModifications())
+ rowBuilder.addCell(live(CommandsForKeyColumns.last_write_timestamp, timestampMicros, serializeTimestamp(cfk.lastWriteTimestamp.get())));
+
+ if (cfk.blindWitnessed.hasModifications())
+ addStoredSetChanges(rowBuilder, CommandsForKeyColumns.blind_witnessed,
+ timestampMicros, nowInSeconds, cfk.blindWitnessed,
+ AccordKeyspace::serializeTimestamp);
+
+ if (updateStaticRow)
+ partitionBuilder.add(rowBuilder.build());
+
+ addSeriesMutations(cfk.uncommitted, partitionBuilder, rowBuilder, timestampMicros, nowInSeconds);
+ addSeriesMutations(cfk.committedById, partitionBuilder, rowBuilder, timestampMicros, nowInSeconds);
+ addSeriesMutations(cfk.committedByExecuteAt, partitionBuilder, rowBuilder, timestampMicros, nowInSeconds);
+
+ return new Mutation(partitionBuilder.build());
+ }
+
+ public static AccordCommandsForKey loadCommandsForKey(AccordCommandStore commandStore, PartitionKey key)
+ {
+ AccordCommandsForKey commandsForKey = new AccordCommandsForKey(commandStore, key);
+ loadCommandsForKey(commandsForKey);
+ return commandsForKey;
+ }
+
+ private static <T> ByteBuffer cellValue(Cell<T> cell)
+ {
+ return cell.accessor().toBuffer(cell.value());
+ }
+
+ // TODO: convert to byte array
+ private static ByteBuffer cellValue(Row row, ColumnMetadata column)
+ {
+ Cell<?> cell = row.getCell(column);
+ return (cell != null && !cell.isTombstone()) ? cellValue(cell) : null;
+ }
+
+ private static <T> ByteBuffer clusteringValue(Clustering<T> clustering, int idx)
+ {
+ return clustering.accessor().toBuffer(clustering.get(idx));
+ }
+
+ public static SinglePartitionReadCommand getCommandsForKeyRead(CommandStore commandStore, PartitionKey key, int nowInSeconds)
+ {
+ return SinglePartitionReadCommand.create(CommandsForKey, nowInSeconds,
+ CommandsForKeyColumns.allColumns,
+ RowFilter.NONE,
+ DataLimits.NONE,
+ makeKey(commandStore, key),
+ FULL_PARTITION);
+ }
+
+ public static void loadCommandsForKey(AccordCommandsForKey cfk)
+ {
+ Preconditions.checkArgument(!cfk.isLoaded());
+ ((AccordCommandStore) cfk.commandStore()).checkNotInStoreThread();
+ long timestampMicros = TimeUnit.MILLISECONDS.toMicros(Clock.Global.currentTimeMillis());
+ int nowInSeconds = (int) TimeUnit.MICROSECONDS.toSeconds(timestampMicros);
+
+ SinglePartitionReadCommand command = getCommandsForKeyRead(cfk.commandStore(), cfk.key(), nowInSeconds);
+
+ EnumMap<SeriesKind, TreeMap<Timestamp, ByteBuffer>> seriesMaps = new EnumMap<>(SeriesKind.class);
+ for (SeriesKind kind : SeriesKind.values())
+ seriesMaps.put(kind, new TreeMap<>());
+
+ try(ReadExecutionController controller = command.executionController();
+ FilteredPartitions partitions = FilteredPartitions.filter(command.executeLocally(controller), nowInSeconds))
+ {
+ if (!partitions.hasNext())
+ {
+ cfk.setEmpty();
+ return;
+ }
+
+ try (RowIterator partition = partitions.next())
+ {
+ // empty static row will be interpreted as all null cells which will cause everything to be initialized
+ Row staticRow = partition.staticRow();
+ Cell<?> cell = staticRow.getCell(CommandsForKeyColumns.max_timestamp);
+ cfk.maxTimestamp.load(cell != null && !cell.isTombstone() ? deserializeTimestampOrNull(cellValue(cell), Timestamp::new)
+ : AccordCommandsForKey.Defaults.maxTimestamp);
+
+ cell = staticRow.getCell(CommandsForKeyColumns.last_executed_timestamp);
+ cfk.lastExecutedTimestamp.load(cell != null && !cell.isTombstone() ? deserializeTimestampOrNull(cellValue(cell), Timestamp::new)
+ : AccordCommandsForKey.Defaults.lastExecutedTimestamp);
+
+ cell = staticRow.getCell(CommandsForKeyColumns.last_executed_micros);
+ ByteBuffer microsBytes = cell != null && !cell.isTombstone() ? cellValue(cell) : null;
+ cfk.lastExecutedMicros.load(microsBytes != null ? microsBytes.getLong(microsBytes.position())
+ : AccordCommandsForKey.Defaults.lastExecutedMicros);
+
+ cell = staticRow.getCell(CommandsForKeyColumns.last_write_timestamp);
+ cfk.lastWriteTimestamp.load(cell != null && !cell.isTombstone() ? deserializeTimestampOrNull(cellValue(cell), Timestamp::new)
+ : AccordCommandsForKey.Defaults.lastWriteTimestamp);
+
+ TreeSet<Timestamp> blindWitnessed = new TreeSet<>();
+ ComplexColumnData cmplx = staticRow.getComplexColumnData(CommandsForKeyColumns.blind_witnessed);
+ if (cmplx != null)
+ cmplx.forEach(c -> blindWitnessed.add(deserializeTimestampOrNull(c.path().get(0), Timestamp::new)));
+ cfk.blindWitnessed.load(blindWitnessed);
+
+ while (partition.hasNext())
+ {
+ Row row = partition.next();
+ Clustering<?> clustering = row.clustering();
+ int ordinal = Int32Type.instance.compose(clusteringValue(clustering, 0));
+ Timestamp timestamp = deserializeTimestampOrNull(clusteringValue(clustering, 1), Timestamp::new);
+ ByteBuffer data = cellValue(row, CommandsForKeyColumns.data);
+ if (data == null)
+ continue;
+ seriesMaps.get(SeriesKind.values()[ordinal]).put(timestamp, data);
+ }
+ }
+ Preconditions.checkState(!partitions.hasNext());
+
+ cfk.uncommitted.map.load(seriesMaps.get(SeriesKind.UNCOMMITTED));
+ cfk.committedById.map.load(seriesMaps.get(SeriesKind.COMMITTED_BY_ID));
+ cfk.committedByExecuteAt.map.load(seriesMaps.get(SeriesKind.COMMITTED_BY_EXECUTE_AT));
+ }
+ catch (Throwable t)
+ {
+ logger.error("Exception loading AccordCommandsForKey " + cfk.key(), t);
+ throw t;
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordMessageSink.java b/src/java/org/apache/cassandra/service/accord/AccordMessageSink.java
new file mode 100644
index 0000000000..b723b27e6e
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordMessageSink.java
@@ -0,0 +1,120 @@
+/*
+ * 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.EnumMap;
+import java.util.Map;
+
+import com.google.common.base.Preconditions;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.api.MessageSink;
+import accord.local.Node;
+import accord.messages.Callback;
+import accord.messages.MessageType;
+import accord.messages.Reply;
+import accord.messages.ReplyContext;
+import accord.messages.Request;
+import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.net.Message;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.net.Verb;
+
+import static org.apache.cassandra.service.accord.EndpointMapping.getEndpoint;
+
+public class AccordMessageSink implements MessageSink
+{
+ private static final Logger logger = LoggerFactory.getLogger(AccordMessageSink.class);
+
+ private static class VerbMapping
+ {
+ private static final VerbMapping instance = new VerbMapping();
+
+ private final Map<MessageType, Verb> mapping = new EnumMap<>(MessageType.class);
+
+ private VerbMapping()
+ {
+ mapping.put(MessageType.PREACCEPT_REQ, Verb.ACCORD_PREACCEPT_REQ);
+ mapping.put(MessageType.PREACCEPT_RSP, Verb.ACCORD_PREACCEPT_RSP);
+ mapping.put(MessageType.ACCEPT_REQ, Verb.ACCORD_ACCEPT_REQ);
+ mapping.put(MessageType.ACCEPT_RSP, Verb.ACCORD_ACCEPT_RSP);
+ mapping.put(MessageType.ACCEPT_INVALIDATE_REQ, Verb.ACCORD_ACCEPT_INVALIDATE_REQ);
+ mapping.put(MessageType.COMMIT_REQ, Verb.ACCORD_COMMIT_REQ);
+ mapping.put(MessageType.COMMIT_INVALIDATE, Verb.ACCORD_COMMIT_INVALIDATE_REQ);
+ mapping.put(MessageType.APPLY_REQ, Verb.ACCORD_APPLY_REQ);
+ mapping.put(MessageType.APPLY_RSP, Verb.ACCORD_APPLY_RSP);
+ mapping.put(MessageType.APPLY_AND_CHECK_REQ, Verb.ACCORD_APPLY_AND_CHECK_REQ);
+ mapping.put(MessageType.APPLY_AND_CHECK_RSP, Verb.ACCORD_APPLY_AND_CHECK_RSP);
+ mapping.put(MessageType.READ_REQ, Verb.ACCORD_READ_REQ);
+ mapping.put(MessageType.READ_RSP, Verb.ACCORD_READ_RSP);
+ mapping.put(MessageType.BEGIN_RECOVER_REQ, Verb.ACCORD_RECOVER_REQ);
+ mapping.put(MessageType.BEGIN_RECOVER_RSP, Verb.ACCORD_RECOVER_RSP);
+ mapping.put(MessageType.BEGIN_INVALIDATE_REQ, Verb.ACCORD_BEGIN_INVALIDATE_REQ);
+ mapping.put(MessageType.BEGIN_INVALIDATE_RSP, Verb.ACCORD_BEGIN_INVALIDATE_RSP);
+ mapping.put(MessageType.WAIT_ON_COMMIT_REQ, Verb.ACCORD_WAIT_COMMIT_REQ);
+ mapping.put(MessageType.WAIT_ON_COMMIT_RSP, Verb.ACCORD_WAIT_COMMIT_RSP);
+ mapping.put(MessageType.INFORM_REQ, Verb.ACCORD_INFORM_OF_TXN_REQ);
+ mapping.put(MessageType.INFORM_RSP, Verb.ACCORD_INFORM_OF_TXN_RSP);
+ mapping.put(MessageType.INFORM_PERSISTED_REQ, Verb.ACCORD_INFORM_OF_PERSIETENCE_REQ);
+ mapping.put(MessageType.INFORM_PERSISTED_RSP, Verb.ACCORD_INFORM_OF_PERSISTENCE_RSP);
+ mapping.put(MessageType.CHECK_STATUS_REQ, Verb.ACCORD_CHECK_STATUS_REQ);
+ mapping.put(MessageType.CHECK_STATUS_RSP, Verb.ACCORD_CHECK_STATUS_RSP);
+ }
+ }
+
+ private static Verb getVerb(MessageType type)
+ {
+ return VerbMapping.instance.mapping.get(type);
+ }
+
+ @Override
+ public void send(Node.Id to, Request request)
+ {
+ Verb verb = getVerb(request.type());
+ Preconditions.checkArgument(verb != null);
+ Message<Request> message = Message.out(verb, request);
+ InetAddressAndPort endpoint = getEndpoint(to);
+ logger.debug("Sending {} {} to {}", verb, message.payload, endpoint);
+ MessagingService.instance().send(message, endpoint);
+ }
+
+ @Override
+ public void send(Node.Id to, Request request, Callback callback)
+ {
+ Verb verb = getVerb(request.type());
+ Preconditions.checkArgument(verb != null);
+ Message<Request> message = Message.out(verb, request);
+ InetAddressAndPort endpoint = getEndpoint(to);
+ logger.debug("Sending {} {} to {}", verb, message.payload, endpoint);
+ MessagingService.instance().sendWithCallback(message, endpoint, new AccordCallback<>((Callback<Reply>) callback));
+ }
+
+ @Override
+ public void reply(Node.Id replyingToNode, ReplyContext replyContext, Reply reply)
+ {
+ Message<?> replyTo = (Message<?>) replyContext;
+ Message<?> replyMsg = replyTo.responseWith(reply);
+ Preconditions.checkArgument(replyMsg.verb() == getVerb(reply.type()));
+ InetAddressAndPort endpoint = getEndpoint(replyingToNode);
+ logger.debug("Replying {} {} to {}", replyMsg.verb(), replyMsg.payload, endpoint);
+ MessagingService.instance().send(replyMsg, endpoint);
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordObjectSizes.java b/src/java/org/apache/cassandra/service/accord/AccordObjectSizes.java
new file mode 100644
index 0000000000..e4ea7049b7
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordObjectSizes.java
@@ -0,0 +1,101 @@
+/*
+ * 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.Map;
+
+import accord.api.Key;
+import accord.local.Node;
+import accord.primitives.Deps;
+import accord.primitives.Keys;
+import accord.primitives.Timestamp;
+import accord.primitives.TxnId;
+import accord.txn.Txn;
+import accord.txn.Writes;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.db.AccordQuery;
+import org.apache.cassandra.service.accord.db.AccordRead;
+import org.apache.cassandra.service.accord.db.AccordUpdate;
+import org.apache.cassandra.service.accord.db.AccordWrite;
+import org.apache.cassandra.utils.ObjectSizes;
+
+public class AccordObjectSizes
+{
+ public static long key(Key key)
+ {
+ return ((AccordKey.PartitionKey) key).estimatedSizeOnHeap();
+ }
+
+ private static final long EMPTY_KEYS_SIZE = ObjectSizes.measure(Keys.of());
+ public static long keys(Keys keys)
+ {
+ long size = EMPTY_KEYS_SIZE;
+ size += ObjectSizes.sizeOfReferenceArray(keys.size());
+ for (int i=0, mi=keys.size(); i<mi; i++)
+ size += key(keys.get(i));
+ return size;
+ }
+
+ private static final long EMPTY_TXN = ObjectSizes.measure(new Txn.InMemory(null, null, null));
+ public static long txn(Txn txn)
+ {
+ long size = EMPTY_TXN;
+ size += keys(txn.keys());
+ size += ((AccordRead) txn.read()).estimatedSizeOnHeap();
+ if (txn.update() != null)
+ size += ((AccordUpdate) txn.update()).estimatedSizeOnHeap();
+ size += ((AccordQuery) txn.query()).estimatedSizeOnHeap();
+ return size;
+ }
+
+ private static final long TIMESTAMP_SIZE = ObjectSizes.measureDeep(new Timestamp(0, 0, 0, new Node.Id(0)));
+
+ public static long timestamp()
+ {
+ return TIMESTAMP_SIZE;
+ }
+ public static long timestamp(Timestamp timestamp)
+ {
+ return TIMESTAMP_SIZE;
+ }
+
+ private static final long EMPTY_DEPS_SIZE = ObjectSizes.measureDeep(Deps.NONE);
+ public static long dependencies(Deps dependencies)
+ {
+ long size = EMPTY_DEPS_SIZE;
+ for (Map.Entry<Key, TxnId> entry : dependencies)
+ {
+ size += key(entry.getKey());
+ size += timestamp(entry.getValue());
+ }
+ return size;
+ }
+
+ private static final long EMPTY_WRITES_SIZE = ObjectSizes.measure(new Writes(null, null, null));
+ public static long writes(Writes writes)
+ {
+ long size = EMPTY_WRITES_SIZE;
+ size += timestamp(writes.executeAt);
+ size += keys(writes.keys);
+ if (writes.write != null)
+ size += ((AccordWrite) writes.write).estimatedSizeOnHeap();
+ return size;
+ }
+
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordPartialCommand.java b/src/java/org/apache/cassandra/service/accord/AccordPartialCommand.java
new file mode 100644
index 0000000000..1419a7595f
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordPartialCommand.java
@@ -0,0 +1,338 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import accord.local.Listener;
+import accord.local.PartialCommand;
+import accord.local.Status;
+import accord.primitives.Deps;
+import accord.primitives.Timestamp;
+import accord.primitives.TxnId;
+import accord.txn.Txn;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.service.accord.async.AsyncContext;
+import org.apache.cassandra.service.accord.serializers.CommandSerializers;
+
+import static org.apache.cassandra.service.accord.serializers.NullableSerializer.deserializeNullable;
+import static org.apache.cassandra.service.accord.serializers.NullableSerializer.serializeNullable;
+import static org.apache.cassandra.service.accord.serializers.NullableSerializer.serializedSizeNullable;
+
+public class AccordPartialCommand implements PartialCommand
+{
+ public static final PartialCommandSerializer<PartialCommand> serializer = new PartialCommandSerializer<PartialCommand>()
+ {
+ @Override
+ public PartialCommand getCachedFull(TxnId txnId, AsyncContext context)
+ {
+ return context.commands.get(txnId);
+ }
+
+ @Override
+ public void addToContext(PartialCommand command, AsyncContext context)
+ {
+ context.commands.addPartialCommand((AccordPartialCommand) command);
+ }
+
+ @Override
+ public PartialCommand deserializeBody(TxnId txnId, Txn txn, Timestamp executeAt, Status status, DataInputPlus in, Version version) throws IOException
+ {
+ return new AccordPartialCommand(txnId, txn, executeAt, status);
+ }
+ };
+
+ private final TxnId txnId;
+ private final Txn txn;
+ private final Timestamp executeAt;
+ private final Status status;
+ private List<Listener> removedListeners = null;
+
+ public AccordPartialCommand(TxnId txnId, Txn txn, Timestamp executeAt, Status status)
+ {
+ this.txnId = txnId;
+ this.txn = txn;
+ this.executeAt = executeAt;
+ this.status = status;
+ }
+
+ @Override
+ public TxnId txnId()
+ {
+ return txnId;
+ }
+
+ @Override
+ public Txn txn()
+ {
+ return txn;
+ }
+
+ @Override
+ public Timestamp executeAt()
+ {
+ return executeAt;
+ }
+
+ @Override
+ public Status status()
+ {
+ return status;
+ }
+
+ @Override
+ public void removeListener(Listener listener)
+ {
+ if (removedListeners == null)
+ removedListeners = new ArrayList<>();
+ removedListeners.add(listener);
+ }
+
+ public boolean hasRemovedListeners()
+ {
+ return removedListeners != null && !removedListeners.isEmpty();
+ }
+
+ public void forEachRemovedListener(Consumer<Listener> consumer)
+ {
+ removedListeners.forEach(consumer);
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AccordPartialCommand that = (AccordPartialCommand) o;
+ return Objects.equals(txnId, that.txnId) && Objects.equals(txn, that.txn) && Objects.equals(executeAt, that.executeAt) && status == that.status && Objects.equals(removedListeners, that.removedListeners);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(txnId, txn, executeAt, status, removedListeners);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "AccordPartialCommand{" +
+ "txnId=" + txnId +
+ ", txn=" + txn +
+ ", executeAt=" + executeAt +
+ ", status=" + status +
+ ", removedListeners=" + removedListeners +
+ '}';
+ }
+
+ public static class WithDeps extends AccordPartialCommand implements PartialCommand.WithDeps
+ {
+ public static final PartialCommandSerializer<PartialCommand.WithDeps> serializer = new PartialCommandSerializer<PartialCommand.WithDeps>()
+ {
+ @Override
+ public PartialCommand.WithDeps getCachedFull(TxnId txnId, AsyncContext context)
+ {
+ return context.commands.get(txnId);
+ }
+
+ @Override
+ public void addToContext(PartialCommand.WithDeps command, AsyncContext context)
+ {
+ context.commands.addPartialCommand((AccordPartialCommand) command);
+ }
+
+ @Override
+ public PartialCommand.WithDeps deserializeBody(TxnId txnId, Txn txn, Timestamp executeAt, Status status, DataInputPlus in, Version version) throws IOException
+ {
+ Deps deps = deserializeNullable(in, version.msg_version, CommandSerializers.deps);
+ return new AccordPartialCommand.WithDeps(txnId, txn, executeAt, status, deps);
+ }
+
+ @Override
+ public void serialize(PartialCommand.WithDeps command, DataOutputPlus out, Version version) throws IOException
+ {
+ super.serialize(command, out, version);
+ serializeNullable(command.savedDeps(), out, version.msg_version, CommandSerializers.deps);
+ }
+
+ @Override
+ public int serializedSize(PartialCommand.WithDeps command, Version version)
+ {
+ int size = super.serializedSize(command, version) ;
+ size += serializedSizeNullable(command.savedDeps(), version.msg_version, CommandSerializers.deps);
+ return size;
+ }
+
+ @Override
+ public boolean needsUpdate(AccordCommand command)
+ {
+ return super.needsUpdate(command) || command.deps.hasModifications();
+ }
+ };
+
+ private final Deps deps;
+
+ public WithDeps(TxnId txnId, Txn txn, Timestamp executeAt, Status status, Deps deps)
+ {
+ super(txnId, txn, executeAt, status);
+ this.deps = deps;
+ }
+
+ @Override
+ public Deps savedDeps()
+ {
+ return deps;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AccordPartialCommand.WithDeps withDeps = (AccordPartialCommand.WithDeps) o;
+ return super.equals(o) && Objects.equals(deps, withDeps.deps);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(super.hashCode(), deps);
+ }
+ }
+
+ public static abstract class PartialCommandSerializer<T extends PartialCommand>
+ {
+ public enum Version
+ {
+ VERSION_0(0, MessagingService.current_version);
+ final byte version;
+ final int msg_version;
+
+ Version(int version, int msg_version)
+ {
+ this.version = (byte) version;
+ this.msg_version = msg_version;
+ }
+
+ public static final Version current = VERSION_0;
+
+ public static Version fromByte(byte b)
+ {
+ switch (b)
+ {
+ case 0:
+ return VERSION_0;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+ }
+
+ public void serialize(T command, DataOutputPlus out, Version version) throws IOException
+ {
+ out.write(version.version);
+ CommandSerializers.txnId.serialize(command.txnId(), out, version.msg_version);
+ CommandSerializers.status.serialize(command.status(), out, version.msg_version);
+ serializeNullable(command.txn(), out, version.msg_version, CommandSerializers.txn);
+ serializeNullable(command.executeAt(), out, version.msg_version, CommandSerializers.timestamp);
+ }
+
+ public ByteBuffer serialize(T command)
+ {
+ Version version = Version.current;
+ int size = serializedSize(command, version);
+ try (DataOutputBuffer out = new DataOutputBuffer(size))
+ {
+ serialize(command, out, version);
+ return out.buffer(false);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Version deserializeVersion(DataInputPlus in) throws IOException
+ {
+ return Version.fromByte(in.readByte());
+ }
+
+ // check for cached command first, otherwise deserialize
+ public T deserialize(AccordCommandStore commandStore, DataInputPlus in) throws IOException
+ {
+ Version version = deserializeVersion(in);
+ TxnId txnId = CommandSerializers.txnId.deserialize(in, version.msg_version);
+ AsyncContext context = commandStore.getContext();
+ T command = getCachedFull(txnId, context);
+ if (command != null)
+ return command;
+
+ Status status = CommandSerializers.status.deserialize(in, version.msg_version);
+ Txn txn = deserializeNullable(in, version.msg_version, CommandSerializers.txn);
+ Timestamp executeAt = deserializeNullable(in, version.msg_version, CommandSerializers.timestamp);
+ T partial = deserializeBody(txnId, txn, executeAt, status, in, version);
+ addToContext(partial, context);
+ return partial;
+ }
+
+ public T deserialize(AccordCommandStore commandStore, ByteBuffer bytes)
+ {
+ try (DataInputBuffer in = new DataInputBuffer(bytes, true))
+ {
+ return deserialize(commandStore, in);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public int serializedSize(T command, Version version)
+ {
+ int size = TypeSizes.sizeof(version.version);
+ size += CommandSerializers.txnId.serializedSize();
+ size += CommandSerializers.status.serializedSize(command.status(), version.msg_version);
+ size += serializedSizeNullable(command.txn(), version.msg_version, CommandSerializers.txn);
+ size += serializedSizeNullable(command.executeAt(), version.msg_version, CommandSerializers.timestamp);
+ return size;
+ }
+
+ public abstract T getCachedFull(TxnId txnId, AsyncContext context);
+ public abstract void addToContext(T command, AsyncContext context);
+ public abstract T deserializeBody(TxnId txnId, Txn txn, Timestamp executeAt, Status status, DataInputPlus in, Version version) throws IOException;
+
+ /**
+ * Determines if current modifications require updating command data duplicated elsewhere
+ */
+ public boolean needsUpdate(AccordCommand command)
+ {
+ return command.txn.hasModifications() || command.executeAt.hasModifications() || command.status.hasModifications();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/service/accord/AccordService.java b/src/java/org/apache/cassandra/service/accord/AccordService.java
new file mode 100644
index 0000000000..02c62510b6
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordService.java
@@ -0,0 +1,187 @@
+/*
+ * 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.Arrays;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import accord.impl.SimpleProgressLog;
+import accord.local.Node;
+import accord.messages.Reply;
+import accord.messages.Request;
+import org.apache.cassandra.concurrent.Shutdownable;
+import org.apache.cassandra.net.IVerbHandler;
+import org.apache.cassandra.service.accord.api.AccordAgent;
+import org.apache.cassandra.service.accord.api.AccordScheduler;
+import org.apache.cassandra.utils.Clock;
+import org.apache.cassandra.utils.ExecutorUtils;
+import org.apache.cassandra.utils.FBUtilities;
+
+public class AccordService implements Shutdownable
+{
+ public final Node node;
+ private final Shutdownable nodeShutdown;
+ private final AccordMessageSink messageSink;
+ public final AccordConfigurationService configService;
+ private final AccordScheduler scheduler;
+ private final AccordVerbHandler verbHandler;
+
+ private static class Handle
+ {
+ public static final AccordService instance = new AccordService();
+ }
+
+ public static AccordService instance()
+ {
+ return Handle.instance;
+ }
+
+ public static long uniqueNow()
+ {
+ return TimeUnit.MILLISECONDS.toMicros(Clock.Global.currentTimeMillis());
+ }
+
+ private AccordService()
+ {
+ Node.Id localId = EndpointMapping.endpointToId(FBUtilities.getBroadcastAddressAndPort());
+ this.messageSink = new AccordMessageSink();
+ this.configService = new AccordConfigurationService(localId);
+ this.scheduler = new AccordScheduler();
+ this.node = new Node(localId,
+ messageSink,
+ configService,
+ AccordService::uniqueNow,
+ () -> null,
+ new AccordAgent(),
+ new Random(),
+ scheduler,
+ SimpleProgressLog::new,
+ AccordCommandStores::new);
+ this.nodeShutdown = toShutdownable(node);
+ this.verbHandler = new AccordVerbHandler(this.node);
+ }
+
+ public <T extends Request> IVerbHandler<T> verbHandler()
+ {
+ return verbHandler;
+ }
+
+ @VisibleForTesting
+ public void createEpochFromConfigUnsafe()
+ {
+ configService.createEpochFromConfig();
+ }
+
+ public static long nowInMicros()
+ {
+ return TimeUnit.MILLISECONDS.toMicros(Clock.Global.currentTimeMillis());
+ }
+
+ @VisibleForTesting
+ AccordMessageSink messageSink()
+ {
+ return messageSink;
+ }
+
+ public void setCacheSize(long kb)
+ {
+ long bytes = kb << 10;
+ AccordCommandStores commandStores = (AccordCommandStores) node.commandStores();
+ commandStores.setCacheSize(bytes);
+ }
+
+ @Override
+ public boolean isTerminated()
+ {
+ return scheduler.isTerminated();
+ }
+
+ @Override
+ public void shutdown()
+ {
+ ExecutorUtils.shutdown(Arrays.asList(scheduler, nodeShutdown));
+ }
+
+ @Override
+ public Object shutdownNow()
+ {
+ ExecutorUtils.shutdownNow(Arrays.asList(scheduler, nodeShutdown));
+ return null;
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException
+ {
+ try
+ {
+ ExecutorUtils.awaitTermination(timeout, units, Arrays.asList(scheduler, nodeShutdown));
+ return true;
+ }
+ catch (TimeoutException e)
+ {
+ return false;
+ }
+ }
+
+ @VisibleForTesting
+ public void shutdownAndWait(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+ {
+ ExecutorUtils.shutdownAndWait(timeout, unit, this);
+ }
+
+ private static Shutdownable toShutdownable(Node node)
+ {
+ return new Shutdownable() {
+ private volatile boolean isShutdown = false;
+
+ @Override
+ public boolean isTerminated()
+ {
+ // we don't know about terminiated... so settle for shutdown!
+ return isShutdown;
+ }
+
+ @Override
+ public void shutdown()
+ {
+ isShutdown = true;
+ node.shutdown();
+ }
+
+ @Override
+ public Object shutdownNow()
+ {
+ // node doesn't offer shutdownNow
+ shutdown();
+ return null;
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException
+ {
+ // node doesn't offer
+ return true;
+ }
+ };
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordState.java b/src/java/org/apache/cassandra/service/accord/AccordState.java
new file mode 100644
index 0000000000..ba755fb1eb
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordState.java
@@ -0,0 +1,113 @@
+/*
+ * 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.function.Function;
+
+import org.apache.cassandra.service.accord.store.StoredNavigableMap;
+import org.apache.cassandra.service.accord.store.StoredSet;
+import org.apache.cassandra.utils.concurrent.Future;
+
+public interface AccordState<K>
+{
+ public enum Kind { FULL, WRITE_ONLY, READ_ONLY }
+
+ K key();
+
+ boolean hasModifications();
+
+ void clearModifiedFlag();
+
+ boolean isEmpty();
+
+ boolean isLoaded();
+
+ long estimatedSizeOnHeap();
+
+ default Kind kind()
+ {
+ return Kind.FULL;
+ }
+
+ default boolean isFullInstance()
+ {
+ return kind() == Kind.FULL;
+ }
+
+ default boolean isWriteOnlyInstance()
+ {
+ return kind() == Kind.WRITE_ONLY;
+ }
+
+ default boolean isReadOnlyInstance()
+ {
+ return kind() == Kind.READ_ONLY;
+ }
+
+ interface WriteOnly<K, V extends AccordState<K>> extends AccordState<K>
+ {
+ @Override
+ default Kind kind()
+ {
+ return Kind.WRITE_ONLY;
+ }
+
+ void future(Future<?> future);
+
+ Future<?> future();
+
+ /**
+ * Apply the write only changes to the full instance
+ */
+ void applyChanges(V instance);
+
+ static <T, K extends Comparable<?>, V> void applyMapChanges(T from, T to, Function<T, StoredNavigableMap<K, V>> getMap)
+ {
+ StoredNavigableMap<K, V> fromMap = getMap.apply(from);
+
+ if (!fromMap.hasModifications())
+ return;
+
+ StoredNavigableMap<K, V> toMap = getMap.apply(to);
+ fromMap.forEachAddition(toMap::blindPut);
+ fromMap.forEachDeletion(toMap::blindRemove);
+ }
+
+ static <T, V extends Comparable<?>> void applySetChanges(T from, T to, Function<T, StoredSet<V, ?>> getSet)
+ {
+ StoredSet<V, ?> fromSet = getSet.apply(from);
+
+ if (!fromSet.hasModifications())
+ return;
+
+ StoredSet<V, ?> toSet = getSet.apply(to);
+ fromSet.forEachAddition(toSet::blindAdd);
+ fromSet.forEachDeletion(toSet::blindRemove);
+ }
+ }
+
+ interface ReadOnly<K, V extends AccordState<K>> extends AccordState<K>
+ {
+ @Override
+ default Kind kind()
+ {
+ return Kind.READ_ONLY;
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordStateCache.java b/src/java/org/apache/cassandra/service/accord/AccordStateCache.java
new file mode 100644
index 0000000000..0a04a6e550
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordStateCache.java
@@ -0,0 +1,646 @@
+/*
+ * 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.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.txn.Txn;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.concurrent.Future;
+import org.apache.cassandra.utils.concurrent.FutureCombiner;
+
+/**
+ * Cache for AccordCommand and AccordCommandsForKey, available memory is shared between the two object types.
+ *
+ * Supports dynamic object sizes. After each acquire/free cycle, the cacheable objects size is recomputed to
+ * account for data added/removed during txn processing if it's modified flag is set
+ *
+ * TODO: explain how items move to and from the active pool and are evicted
+ */
+public class AccordStateCache
+{
+ private static final Logger logger = LoggerFactory.getLogger(AccordStateCache.class);
+
+ private static class WriteOnlyGroup<K, V extends AccordState<K>>
+ {
+ private boolean locked = false;
+ private List<AccordState.WriteOnly<K, V>> items = new ArrayList<>();
+
+ @Override
+ public String toString()
+ {
+ return "WriteOnlyGroup{" +
+ "locked=" + locked +
+ ", items=" + items +
+ '}';
+ }
+
+ void lock()
+ {
+ locked = true;
+ }
+
+ void add(AccordState.WriteOnly<K, V> item)
+ {
+ items.add(item);
+ }
+
+ void purge()
+ {
+ if (locked)
+ return;
+
+ while (!items.isEmpty())
+ {
+ AccordState.WriteOnly<K, V> item = items.get(0);
+
+ // we can't remove items out of order, so if we encounter a write is still pending, we stop
+ if (item.future() == null || !item.future().isDone())
+ break;
+
+ items.remove(0);
+ }
+ }
+
+ boolean isEmpty()
+ {
+ return items.isEmpty();
+ }
+ }
+
+ static class Node<K, V extends AccordState<K>>
+ {
+ static final long EMPTY_SIZE = ObjectSizes.measure(new AccordStateCache.Node<>(null));
+
+ final V value;
+ private Node<?, ?> prev;
+ private Node<?, ?> next;
+ private int references = 0;
+ private long lastQueriedSize = 0;
+
+ Node(V value)
+ {
+ this.value = value;
+ }
+
+ long size()
+ {
+ long result = EMPTY_SIZE + value.estimatedSizeOnHeap();
+ lastQueriedSize = result;
+ return result;
+ }
+
+ long sizeDelta()
+ {
+ long prevSize = lastQueriedSize;
+ return size() - prevSize;
+ }
+
+ K key()
+ {
+ return value.key();
+ }
+ }
+
+ static class Stats
+ {
+ private long queries;
+ private long hits;
+ private long misses;
+ }
+
+ private static class NamedMap<K, V> extends HashMap<K, V>
+ {
+ final String name;
+
+ public NamedMap(String name)
+ {
+ this.name = name;
+ }
+ }
+
+ public final Map<Object, Node<?, ?>> active = new HashMap<>();
+ private final Map<Object, Node<?, ?>> cache = new HashMap<>();
+ private final Map<Object, WriteOnlyGroup<?, ?>> pendingWriteOnly = new HashMap<>();
+ private final Set<Instance<?, ?>> instances = new HashSet<>();
+
+ private final NamedMap<Object, Future<?>> loadFutures = new NamedMap<>("loadFutures");
+ private final NamedMap<Object, Future<?>> saveFutures = new NamedMap<>("saveFutures");
+
+ private final NamedMap<Object, ReadFuture> readFutures = new NamedMap<>("readFutures");
+ private final NamedMap<Object, Future<?>> writeFutures = new NamedMap<>("writeFutures");
+
+ Node<?, ?> head;
+ Node<?, ?> tail;
+ private long maxSizeInBytes;
+ private long bytesCached = 0;
+ private final Stats stats = new Stats();
+
+ public AccordStateCache(long maxSizeInBytes)
+ {
+ this.maxSizeInBytes = maxSizeInBytes;
+ }
+
+ public void setMaxSize(long size)
+ {
+ maxSizeInBytes = size;
+ maybeEvict();
+ }
+
+ private void unlink(Node<?, ?> node)
+ {
+ Node<?, ?> prev = node.prev;
+ Node<?, ?> next = node.next;
+
+ if (prev == null)
+ {
+ Preconditions.checkState(head == node);
+ head = next;
+ }
+ else
+ {
+ prev.next = next;
+ }
+
+ if (next == null)
+ {
+ Preconditions.checkState(tail == node);
+ tail = prev;
+ }
+ else
+ {
+ next.prev = prev;
+ }
+
+ node.prev = null;
+ node.next = null;
+ }
+
+ private void push(Node<?, ?> node)
+ {
+ if (head != null)
+ {
+ node.prev = null;
+ node.next = head;
+ head.prev = node;
+ head = node;
+ }
+ else
+ {
+ head = node;
+ tail = node;
+ }
+ }
+
+ private void updateSize(Node<?, ?> node)
+ {
+ bytesCached += node.sizeDelta();
+ }
+
+ // don't evict if there's an outstanding save future. If an item is evicted then reloaded
+ // before it's mutation is applied, out of date info will be loaded
+ private boolean canEvict(Object key)
+ {
+ Future<?> future = getFuture(saveFutures, key);
+ return future == null || future.isDone();
+ }
+
+ private void maybeEvict()
+ {
+ if (bytesCached <= maxSizeInBytes)
+ return;
+
+ Node<?, ?> current = tail;
+ while (current != null && bytesCached > maxSizeInBytes)
+ {
+ Node<?, ?> evict = current;
+ current = current.prev;
+
+ // if there are any dangling write only groups, apply them and
+ // move their futures into write futures so we don't evict
+ applyAndRemoveWriteOnlyGroup(evict.value);
+ if (!canEvict(evict.key()))
+ continue;
+
+ logger.trace("Evicting {} {}", evict.value.getClass().getSimpleName(), evict.key());
+ unlink(evict);
+ cache.remove(evict.key());
+ bytesCached -= evict.size();
+ }
+ }
+
+ private static <K, F extends Future<?>> F getFuture(NamedMap<Object, F> futuresMap, K key)
+ {
+ F r = futuresMap.get(key);
+ if (r == null)
+ return null;
+
+ if (!r.isDone())
+ return r;
+
+ if (logger.isTraceEnabled())
+ logger.trace("Clearing future for {} from {}: {}", key, futuresMap.name, r);
+ futuresMap.remove(key);
+ return null;
+ }
+
+ private static <K, F extends Future<?>> void setFuture(Map<Object, F> futuresMap, K key, F future)
+ {
+ Preconditions.checkState(!futuresMap.containsKey(key));
+ futuresMap.put(key, future);
+ }
+
+ private static <K> void mergeFuture(Map<Object, Future<?>> futuresMap, K key, Future<?> future)
+ {
+ Future<?> existing = futuresMap.get(key);
+ if (existing != null && !existing.isDone())
+ {
+ logger.trace("Merging future {} with existing {}", future, existing);
+ future = FutureCombiner.allOf(ImmutableList.of(existing, future));
+ }
+
+ futuresMap.put(key, future);
+ }
+
+ private <K> void maybeClearFuture(K key)
+ {
+ // will clear if it's done
+ getFuture(loadFutures, key);
+ getFuture(saveFutures, key);
+ getFuture(readFutures, key);
+ getFuture(writeFutures, key);
+ }
+
+ public <K, V extends AccordState<K>> void applyAndRemoveWriteOnlyGroup(V instance)
+ {
+ WriteOnlyGroup<K, V> group = (WriteOnlyGroup<K, V>) pendingWriteOnly.remove(instance.key());
+ if (group == null)
+ return;
+
+ logger.trace("Applying and removing write only group for {} ({})", instance.key(), group);
+ for (AccordState.WriteOnly<K, V> writeOnly : group.items)
+ {
+ writeOnly.applyChanges(instance);
+ if (!writeOnly.future().isDone())
+ mergeFuture(saveFutures, instance.key(), writeOnly.future());
+ }
+ }
+
+ public class Instance<K, V extends AccordState<K>>
+ {
+ private final Class<K> keyClass;
+ private final Class<V> valClass;
+ private final Function<K, V> factory;
+ private final Stats stats = new Stats();
+
+ public Instance(Class<K> keyClass, Class<V> valClass, Function<K, V> factory)
+ {
+ this.keyClass = keyClass;
+ this.valClass = valClass;
+ this.factory = factory;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Instance<?, ?> instance = (Instance<?, ?>) o;
+ return keyClass.equals(instance.keyClass) && valClass.equals(instance.valClass);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(keyClass, valClass);
+ }
+
+ private V getOrCreate(K key, boolean createIfAbsent)
+ {
+ stats.queries++;
+ AccordStateCache.this.stats.queries++;
+
+ Node<K, V> node = (Node<K, V>) active.get(key);
+ if (node != null)
+ {
+ stats.hits++;
+ AccordStateCache.this.stats.hits++;
+ node.references++;
+ return node.value;
+ }
+
+ node = (Node<K, V>) cache.remove(key);
+
+ if (node == null)
+ {
+ stats.misses++;
+ AccordStateCache.this.stats.misses++;
+ if (!createIfAbsent)
+ return null;
+ V value = factory.apply(key);
+ node = new Node<>(value);
+ updateSize(node);
+ }
+ else
+ {
+ stats.hits++;
+ AccordStateCache.this.stats.hits++;
+ unlink(node);
+ }
+
+ Preconditions.checkState(node.references == 0);
+ maybeEvict();
+
+ node.references++;
+ active.put(key, node);
+
+ return node.value;
+ }
+
+ public V getOrCreate(K key)
+ {
+ return getOrCreate(key, true);
+ }
+
+ public V getOrNull(K key)
+ {
+ return getOrCreate(key, false);
+ }
+
+ public void release(V value)
+ {
+ K key = value.key();
+ logger.trace("Releasing resources for {}: {}", key, value);
+ maybeClearFuture(key);
+ Node<K, V> node = (Node<K, V>) active.get(key);
+ Preconditions.checkState(node != null && node.references > 0);
+ Preconditions.checkState(node.value == value);
+ if (--node.references == 0)
+ {
+ logger.trace("Moving {} from active pool to cache", key);
+ active.remove(key);
+ cache.put(key, node);
+ push(node);
+ }
+
+ if (value.hasModifications())
+ {
+ value.clearModifiedFlag();
+ updateSize(node);
+ }
+ maybeEvict();
+ }
+
+ @VisibleForTesting
+ boolean canEvict(K key)
+ {
+ return AccordStateCache.this.canEvict(key);
+ }
+
+ @VisibleForTesting
+ boolean writeOnlyGroupIsLocked(K key)
+ {
+ WriteOnlyGroup<K, V> group = (WriteOnlyGroup<K, V>) pendingWriteOnly.get(key);
+ return group != null && group.locked;
+ }
+
+ @VisibleForTesting
+ int pendingWriteOnlyOperations(K key)
+ {
+ WriteOnlyGroup<K, V> group = (WriteOnlyGroup<K, V>) pendingWriteOnly.get(key);
+ return group != null ? group.items.size() : 0;
+ }
+
+ public void lockWriteOnlyGroupIfExists(K key)
+ {
+ WriteOnlyGroup<K, V> group = (WriteOnlyGroup<K, V>) pendingWriteOnly.get(key);
+ if (group == null)
+ return;
+
+ logger.trace("Locking write only group for {} ({})", key, group);
+ group.purge();
+ if (!group.isEmpty())
+ group.lock();
+ }
+
+ public void applyAndRemoveWriteOnlyGroup(V instance)
+ {
+ AccordStateCache.this.applyAndRemoveWriteOnlyGroup(instance);
+ }
+
+ public void addWriteOnly(AccordState.WriteOnly<K, V> writeOnly)
+ {
+ K key = writeOnly.key();
+ Preconditions.checkArgument(writeOnly.future() != null);
+ WriteOnlyGroup<K, V> group = (WriteOnlyGroup<K, V>) pendingWriteOnly.computeIfAbsent(key, k -> new WriteOnlyGroup<>());
+
+ // if a load future exists for the key we're creating a write group for, we need to lock
+ // the group so the loading instance gets changes applied when it finishes loading
+ if (getLoadFuture(key) != null)
+ group.lock();
+
+ group.add(writeOnly);
+ }
+
+ public void purgeWriteOnly(K key)
+ {
+ WriteOnlyGroup<?, ?> items = pendingWriteOnly.get(key);
+ if (items == null)
+ return;
+
+ items.purge();
+ if (items.isEmpty())
+ pendingWriteOnly.remove(key);
+ }
+
+ public boolean writeOnlyGroupExists(K key)
+ {
+ return pendingWriteOnly.get(key) != null;
+ }
+
+ public int getWriteOnlyGroupSize(K key)
+ {
+ WriteOnlyGroup<?, ?> group = pendingWriteOnly.get(key);
+ return group != null ? group.items.size() : 0;
+ }
+
+ public Future<?> getLoadFuture(K key)
+ {
+ return getFuture(loadFutures, key);
+ }
+
+ public void cleanupLoadFuture(K key)
+ {
+ getLoadFuture(key);
+ }
+
+ @VisibleForTesting
+ public boolean hasLoadFuture(K key)
+ {
+ return loadFutures.get(key) != null;
+ }
+
+ public void setLoadFuture(K key, Future<?> future)
+ {
+ setFuture(loadFutures, key, future);
+ }
+
+ public Future<?> getSaveFuture(K key)
+ {
+ return getFuture(saveFutures, key);
+ }
+
+ public void addSaveFuture(K key, Future<?> future)
+ {
+ logger.trace("Adding save future for {}: {}", key, future);
+ mergeFuture(saveFutures, key, future);
+ }
+
+ public void cleanupSaveFuture(K key)
+ {
+ getSaveFuture(key);
+ }
+
+ @VisibleForTesting
+ public boolean hasSaveFuture(K key)
+ {
+ return saveFutures.get(key) != null;
+ }
+
+ public ReadFuture getReadFuture(K key)
+ {
+ return getFuture(readFutures, key);
+ }
+
+ public void setReadFuture(K key, ReadFuture future)
+ {
+ setFuture(readFutures, key, future);
+ }
+
+ public void cleanupReadFuture(K key)
+ {
+ getReadFuture(key);
+ }
+
+ public Future<Void> getWriteFuture(K key)
+ {
+ return (Future<Void>) getFuture(writeFutures, key);
+ }
+
+ public void setWriteFuture(K key, Future<Void> future)
+ {
+ setFuture(writeFutures, key, future);
+ }
+
+ public void cleanupWriteFuture(K key)
+ {
+ getWriteFuture(key);
+ }
+
+ public long cacheQueries()
+ {
+ return stats.queries;
+ }
+
+ public long cacheHits()
+ {
+ return stats.hits;
+ }
+
+ public long cacheMisses()
+ {
+ return stats.misses;
+ }
+ }
+
+ public <K, V extends AccordState<K>> Instance<K, V> instance(Class<K> keyClass, Class<V> valClass, Function<K, V> factory)
+ {
+ Instance<K, V> instance = new Instance<>(keyClass, valClass, factory);
+ if (!instances.add(instance))
+ throw new IllegalArgumentException(String.format("Cache instances for types %s -> %s already exists",
+ keyClass.getName(), valClass.getName()));
+ return instance;
+ }
+
+ @VisibleForTesting
+ int numActiveEntries()
+ {
+ return active.size();
+ }
+
+ @VisibleForTesting
+ int numCachedEntries()
+ {
+ return cache.size();
+ }
+
+ @VisibleForTesting
+ long bytesCached()
+ {
+ return bytesCached;
+ }
+
+ @VisibleForTesting
+ boolean keyIsActive(Object key)
+ {
+ return active.containsKey(key);
+ }
+
+ @VisibleForTesting
+ boolean keyIsCached(Object key)
+ {
+ return cache.containsKey(key);
+ }
+
+ @VisibleForTesting
+ int references(Object key)
+ {
+ Node<?, ?> node = active.get(key);
+ return node != null ? node.references : 0;
+ }
+
+ public long cacheQueries()
+ {
+ return stats.queries;
+ }
+
+ public long cacheHits()
+ {
+ return stats.hits;
+ }
+
+ public long cacheMisses()
+ {
+ return stats.misses;
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordTopologyUtils.java b/src/java/org/apache/cassandra/service/accord/AccordTopologyUtils.java
new file mode 100644
index 0000000000..332a51379a
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordTopologyUtils.java
@@ -0,0 +1,136 @@
+/*
+ * 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.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Preconditions;
+
+import accord.topology.Shard;
+import accord.topology.Topology;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.locator.AbstractReplicationStrategy;
+import org.apache.cassandra.locator.EndpointsForToken;
+import org.apache.cassandra.locator.TokenMetadata;
+import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.SchemaConstants;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.service.accord.api.AccordKey.SentinelKey;
+import org.apache.cassandra.service.accord.api.AccordKey.TokenKey;
+
+public class AccordTopologyUtils
+{
+ private static Shard createShard(TokenRange range, EndpointsForToken natural, EndpointsForToken pending)
+ {
+ return new Shard(range,
+ natural.stream().map(EndpointMapping::getId).collect(Collectors.toList()),
+ natural.stream().map(EndpointMapping::getId).collect(Collectors.toSet()),
+ pending.stream().map(EndpointMapping::getId).collect(Collectors.toSet()));
+ }
+
+ private static TokenRange minRange(TableId tableId, Token token)
+ {
+ return new TokenRange(SentinelKey.min(tableId), TokenKey.max(tableId, token));
+ }
+
+ private static TokenRange maxRange(TableId tableId, Token token)
+ {
+ return new TokenRange(TokenKey.max(tableId, token), SentinelKey.max(tableId));
+ }
+
+ private static TokenRange range(TableId tableId, Token left, Token right)
+ {
+ return new TokenRange(TokenKey.max(tableId, left), TokenKey.max(tableId, right));
+ }
+
+ public static List<Shard> createShards(TableMetadata tableMetadata, TokenMetadata tokenMetadata)
+ {
+ TableId tableId = tableMetadata.id;
+ String keyspace = tableMetadata.keyspace;
+
+ AbstractReplicationStrategy replication = Keyspace.open(keyspace).getReplicationStrategy();
+ Set<Token> tokenSet = new HashSet<>(tokenMetadata.sortedTokens());
+ tokenSet.addAll(tokenMetadata.getBootstrapTokens().keySet());
+ tokenMetadata.getMovingEndpoints().forEach(p -> tokenSet.add(p.left));
+ List<Token> tokens = new ArrayList<>(tokenSet);
+ tokens.sort(Comparator.naturalOrder());
+
+ List<Shard> shards = new ArrayList<>(tokens.size() + 1);
+ Shard finalShard = null;
+ for (int i=0, mi=tokens.size(); i<mi; i++)
+ {
+ Token token = tokens.get(i);
+ EndpointsForToken natural = replication.getNaturalReplicasForToken(token);
+ EndpointsForToken pending = tokenMetadata.pendingEndpointsForToken(token, keyspace);
+ if (i == 0)
+ {
+ shards.add(createShard(minRange(tableId, token), natural, pending));
+ finalShard = createShard(maxRange(tableId, tokens.get(mi-1)), natural, pending);
+ }
+ else
+ {
+ Token prev = tokens.get(i - 1);
+ shards.add(createShard(range(tableId, prev, token), natural, pending));
+ }
+ }
+ shards.add(finalShard);
+
+ return shards;
+ }
+
+ public static Topology createTopology(long epoch)
+ {
+ List<TableId> tableIds = new ArrayList<>();
+ TokenMetadata tokenMetadata = StorageService.instance.getTokenMetadata();
+ for (String ksname: Schema.instance.getKeyspaces())
+ {
+ // TODO: add a table metadata flag to enable and enforce accord use
+ if (SchemaConstants.LOCAL_SYSTEM_KEYSPACE_NAMES.contains(ksname))
+ continue;
+ if (SchemaConstants.REPLICATED_SYSTEM_KEYSPACE_NAMES.contains(ksname))
+ continue;
+
+ Keyspace keyspace = Keyspace.open(ksname);
+ for (TableMetadata tableMetadata : keyspace.getMetadata().tables)
+ {
+ tableIds.add(tableMetadata.id);
+ }
+ }
+
+ tableIds.sort(Comparator.naturalOrder());
+
+ List<Shard> shards = new ArrayList<>();
+ for (TableId tableId : tableIds)
+ {
+ TableMetadata tableMetadata = Schema.instance.getTableMetadata(tableId);
+ Preconditions.checkNotNull(tableMetadata);
+ shards.addAll(createShards(tableMetadata, tokenMetadata));
+ }
+
+ return new Topology(epoch, shards.toArray(new Shard[0]));
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordTxnBuilder.java b/src/java/org/apache/cassandra/service/accord/AccordTxnBuilder.java
new file mode 100644
index 0000000000..1eb8b3915f
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordTxnBuilder.java
@@ -0,0 +1,209 @@
+/*
+ * 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.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Preconditions;
+
+import accord.api.Key;
+import accord.primitives.Keys;
+import accord.txn.Txn;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.QueryHandler;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.statements.ModificationStatement;
+import org.apache.cassandra.cql3.statements.SelectStatement;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.IMutation;
+import org.apache.cassandra.db.ReadQuery;
+import org.apache.cassandra.db.SinglePartitionReadCommand;
+import org.apache.cassandra.db.SinglePartitionReadQuery;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.db.AccordQuery;
+import org.apache.cassandra.service.accord.db.AccordRead;
+import org.apache.cassandra.service.accord.db.AccordUpdate;
+import org.apache.cassandra.service.accord.db.AccordUpdate.UpdatePredicate;
+import org.apache.cassandra.service.accord.db.AccordUpdate.UpdatePredicate.Type;
+
+public class AccordTxnBuilder
+{
+ private Set<AccordKey.PartitionKey> keys = new HashSet<>();
+ private List<SinglePartitionReadCommand> reads = new ArrayList<>();
+ private AccordQuery query = AccordQuery.ALL;
+ private List<AccordUpdate.AbstractUpdate> updates = new ArrayList<>();
+ private List<UpdatePredicate> predicates = new ArrayList<>();
+
+ public AccordTxnBuilder withRead(String query, Object... values)
+ {
+ QueryHandler.Prepared prepared = QueryProcessor.prepareInternal(query);
+ Preconditions.checkArgument(prepared.statement instanceof SelectStatement);
+
+ SelectStatement select = (SelectStatement)prepared.statement;
+ ReadQuery readQuery = select.getQuery(QueryProcessor.makeInternalOptions(prepared.statement, values), 0);
+ SinglePartitionReadQuery.Group<SinglePartitionReadCommand> selectQuery = (SinglePartitionReadQuery.Group<SinglePartitionReadCommand>) readQuery;
+ for (SinglePartitionReadCommand command : selectQuery.queries)
+ {
+ keys.add(AccordKey.of(command));
+ reads.add(command);
+ }
+ return this;
+ }
+
+ public AccordTxnBuilder withWrite(String query, Object... values)
+ {
+ QueryHandler.Prepared prepared = QueryProcessor.prepareInternal(query);
+ Preconditions.checkArgument(prepared.statement instanceof ModificationStatement);
+ ModificationStatement modification = (ModificationStatement) prepared.statement;
+ // TODO: look into getting partition updates directly
+ Preconditions.checkArgument(!modification.requiresRead());
+ List<? extends IMutation> mutations = modification.getMutations(ClientState.forInternalCalls(), QueryProcessor.makeInternalOptions(prepared.statement, values), false, 0, 0, 0);
+ for (IMutation mutation : mutations)
+ {
+ for (PartitionUpdate update : mutation.getPartitionUpdates())
+ {
+ keys.add(new AccordKey.PartitionKey(update.metadata().id, update.partitionKey()));
+ updates.add(new AccordUpdate.SimpleUpdate(update));
+ }
+ }
+ return this;
+ }
+
+ public AccordTxnBuilder withAppend(String keyspace, String table, Object key, Map<String, String> appends)
+ {
+ TableMetadata metadata = Schema.instance.getTableMetadata(keyspace, table);
+ Preconditions.checkNotNull(metadata);
+
+ DecoratedKey partitionKey = metadata.partitioner.decorateKey(decompose(metadata.partitionKeyType, key));
+ AccordKey.PartitionKey accordKey = new AccordKey.PartitionKey(metadata.id, partitionKey);
+
+ keys.add(accordKey);
+ updates.add(new AccordUpdate.AppendingUpdate(accordKey, appends));
+
+ return this;
+ }
+
+ public AccordTxnBuilder withAppend(String keyspace, String table, Object key, String column, String append)
+ {
+ Map<String, String> appends = new HashMap<>();
+ appends.put(column, append);
+ return withAppend(keyspace, table, key, appends);
+ }
+
+ public AccordTxnBuilder withIncrement(String keyspace, String table, Object key, Map<String, Integer> increments)
+ {
+ TableMetadata metadata = Schema.instance.getTableMetadata(keyspace, table);
+ Preconditions.checkNotNull(metadata);
+
+ DecoratedKey partitionKey = metadata.partitioner.decorateKey(decompose(metadata.partitionKeyType, key));
+ AccordKey.PartitionKey accordKey = new AccordKey.PartitionKey(metadata.id, partitionKey);
+
+ keys.add(accordKey);
+ updates.add(new AccordUpdate.IncrementingUpdate(accordKey, increments));
+
+ return this;
+ }
+
+ public AccordTxnBuilder withIncrement(String keyspace, String table, Object key, String column, int increment)
+ {
+ Map<String, Integer> increments = new HashMap<>();
+ increments.put(column, increment);
+ return withIncrement(keyspace, table, key, increments);
+ }
+
+
+ private static ByteBuffer decompose(AbstractType<?> type, Object value)
+ {
+ return ((AbstractType<Object>) type).decompose(value);
+ }
+
+ public AccordTxnBuilder withCondition(String keyspace, String table, Object key, Object clustering, String column, Type type, Object value)
+ {
+ Preconditions.checkState(!updates.isEmpty());
+ boolean isExistsPredicate = type == Type.EXISTS || type == Type.NOT_EXISTS;
+ Preconditions.checkArgument((value != null && !isExistsPredicate) || (value == null && isExistsPredicate));
+
+ TableMetadata metadata = Schema.instance.getTableMetadata(keyspace, table);
+ Preconditions.checkNotNull(metadata);
+
+ DecoratedKey decoratedKey = metadata.partitioner.decorateKey(decompose(metadata.partitionKeyType, key));
+ Clustering<ByteBuffer> clusteringBytes = Clustering.make(decompose(metadata.comparator.subtype(0), clustering));
+
+ if (isExistsPredicate)
+ {
+ predicates.add(new AccordUpdate.ExistsPredicate(type, metadata, decoratedKey, clusteringBytes));
+ }
+ else
+ {
+ ColumnMetadata columnMetadata = metadata.getColumn(new ColumnIdentifier(column, true));
+ Preconditions.checkNotNull(columnMetadata);
+ ByteBuffer valueBytes = decompose(columnMetadata.type, value);
+ predicates.add(new AccordUpdate.ValuePredicate(type, metadata, decoratedKey, clusteringBytes, columnMetadata, valueBytes));
+ }
+
+ return this;
+ }
+
+ public AccordTxnBuilder withCondition(String keyspace, String table, Object key, Object clustering, Type type)
+ {
+ return withCondition(keyspace, table, key, clustering, null, type, null);
+ }
+
+ public Txn build()
+ {
+ Key[] keyArray = keys.toArray(new Key[0]);
+ Arrays.sort(keyArray, AccordKey::compareKeys);
+ predicates.sort(Comparator.comparing(UpdatePredicate::partitionKey));
+ if (updates.isEmpty())
+ {
+ return new Txn.InMemory(Keys.of(keyArray), new AccordRead(reads), query);
+ }
+ else
+ {
+ List<UpdatePredicate> unsatisfiable = predicates.stream()
+ .filter(predicate -> !reads.stream().anyMatch(predicate::supportedByRead))
+ .collect(Collectors.toList());
+
+ if (!unsatisfiable.isEmpty())
+ {
+ String msg = String.format("Some predicates are unsupported by a read, and are unsatisfiable: %s", unsatisfiable);
+ throw new IllegalStateException(msg);
+ }
+
+ return new Txn.InMemory(Keys.of(keyArray), new AccordRead(reads), query, new AccordUpdate(updates, predicates));
+ }
+ }
+
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/shared/VersionedApplicationState.java b/src/java/org/apache/cassandra/service/accord/AccordVerbHandler.java
similarity index 50%
copy from test/distributed/org/apache/cassandra/distributed/shared/VersionedApplicationState.java
copy to src/java/org/apache/cassandra/service/accord/AccordVerbHandler.java
index fd3e40a70d..216d64df42 100644
--- a/test/distributed/org/apache/cassandra/distributed/shared/VersionedApplicationState.java
+++ b/src/java/org/apache/cassandra/service/accord/AccordVerbHandler.java
@@ -16,20 +16,33 @@
* limitations under the License.
*/
-package org.apache.cassandra.distributed.shared;
+package org.apache.cassandra.service.accord;
-import java.io.Serializable;
+import java.io.IOException;
-public class VersionedApplicationState implements Serializable
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.local.Node;
+import accord.messages.Request;
+import org.apache.cassandra.net.IVerbHandler;
+import org.apache.cassandra.net.Message;
+
+public class AccordVerbHandler<T extends Request> implements IVerbHandler<T>
{
- public final int applicationState;
- public final String value;
- public final int version;
+ private static final Logger logger = LoggerFactory.getLogger(AccordVerbHandler.class);
+
+ private final Node node;
+
+ public AccordVerbHandler(Node node)
+ {
+ this.node = node;
+ }
- public VersionedApplicationState(int applicationState, String value, int version)
+ @Override
+ public void doVerb(Message<T> message) throws IOException
{
- this.applicationState = applicationState;
- this.value = value;
- this.version = version;
+ logger.debug("Receiving {} from {}", message.payload, message.from());
+ message.payload.process(node, EndpointMapping.getId(message.from()), message);
}
}
diff --git a/src/java/org/apache/cassandra/service/accord/EndpointMapping.java b/src/java/org/apache/cassandra/service/accord/EndpointMapping.java
new file mode 100644
index 0000000000..d650db1abe
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/EndpointMapping.java
@@ -0,0 +1,145 @@
+/*
+ * 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.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+
+import accord.local.Node;
+import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.locator.Replica;
+
+public class EndpointMapping
+{
+ static Node.Id endpointToId(InetAddressAndPort endpoint)
+ {
+ Preconditions.checkArgument(endpoint.getAddress() instanceof Inet4Address);
+ Inet4Address address = (Inet4Address) endpoint.getAddress();
+ byte[] bytes = address.getAddress();
+ long id = 0;
+ for (int i=0; i<4; i++)
+ id = (id * 1000) + Byte.toUnsignedLong(bytes[i]);
+ id = (id * 100000) + endpoint.getPort();
+ return new Node.Id(id);
+ }
+
+ static InetAddressAndPort idToEndpoint(Node.Id node)
+ {
+ long id = node.id;
+ Preconditions.checkArgument(id >= 0);
+
+ int port = (int) (id % 100000);
+ id = id / 100000;
+ byte[] bytes = new byte[4];
+ for (int i=0; i<4; i++)
+ {
+ long octet = id % 1000;
+ Preconditions.checkArgument(octet >= 0 && octet <= 255, "Malformed id");
+ bytes[3-i] = (byte) (octet);
+ id = id / 1000;
+ }
+ Preconditions.checkArgument(id == 0);
+ try
+ {
+ return InetAddressAndPort.getByAddressOverrideDefaults(InetAddress.getByAddress(bytes), port);
+ }
+ catch (UnknownHostException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static class Mapping
+ {
+ private static final Mapping EMPTY = new Mapping(ImmutableMap.of(), ImmutableMap.of());
+ final ImmutableMap<Node.Id, InetAddressAndPort> idToEndpoint;
+ final ImmutableMap<InetAddressAndPort, Node.Id> endpointToId;
+
+ public Mapping(ImmutableMap<Node.Id, InetAddressAndPort> idToEndpoint,
+ ImmutableMap<InetAddressAndPort, Node.Id> endpointToId)
+ {
+ this.idToEndpoint = idToEndpoint;
+ this.endpointToId = endpointToId;
+ }
+
+ private static <K, V> ImmutableMap<K, V> put(ImmutableMap<K, V> current, K key, V val)
+ {
+ return ImmutableMap.<K, V>builderWithExpectedSize(current.size() + 1).putAll(current).put(key, val).build();
+ }
+
+ public Mapping add(InetAddressAndPort endpoint)
+ {
+ if (endpointToId.containsKey(endpoint))
+ return this;
+ Node.Id id = endpointToId(endpoint);
+ return new Mapping(put(idToEndpoint, id, endpoint), put(endpointToId, endpoint, id));
+ }
+
+ public Mapping add(Node.Id id)
+ {
+ if (idToEndpoint.containsKey(id))
+ return this;
+
+ InetAddressAndPort endpoint = idToEndpoint(id);
+ return new Mapping(put(idToEndpoint, id, endpoint), put(endpointToId, endpoint, id));
+ }
+ }
+
+ private static volatile Mapping mapping = Mapping.EMPTY;
+
+ private EndpointMapping() {}
+
+ public static Node.Id getId(InetAddressAndPort endpoint)
+ {
+ Node.Id id = mapping.endpointToId.get(endpoint);
+ if (id == null)
+ {
+ synchronized (EndpointMapping.class)
+ {
+ mapping = mapping.add(endpoint);
+ id = mapping.endpointToId.get(endpoint);
+ }
+ }
+ return id;
+ }
+
+ // FIXME: put this stuff into the configuration service, where it will eventually live
+ public static Node.Id getId(Replica replica)
+ {
+ return getId(replica.endpoint());
+ }
+
+ public static InetAddressAndPort getEndpoint(Node.Id id)
+ {
+ InetAddressAndPort endpoint = mapping.idToEndpoint.get(id);
+ if (endpoint == null)
+ {
+ synchronized (EndpointMapping.class)
+ {
+ mapping = mapping.add(id);
+ endpoint = mapping.idToEndpoint.get(id);
+ }
+ }
+ return endpoint;
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/ListenerProxy.java b/src/java/org/apache/cassandra/service/accord/ListenerProxy.java
new file mode 100644
index 0000000000..2c9c5e07a9
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/ListenerProxy.java
@@ -0,0 +1,279 @@
+/*
+ * 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.Collections;
+import java.util.Objects;
+
+import com.google.common.collect.ImmutableList;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.local.Command;
+import accord.local.CommandStore;
+import accord.local.Listener;
+import accord.local.PreLoadContext;
+import accord.primitives.TxnId;
+import org.apache.cassandra.db.marshal.ByteBufferAccessor;
+import org.apache.cassandra.db.marshal.ValueAccessor;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.async.AsyncContext;
+import org.apache.cassandra.service.accord.serializers.CommandSerializers;
+import org.apache.cassandra.utils.ObjectSizes;
+
+public abstract class ListenerProxy implements Listener, Comparable<ListenerProxy>
+{
+ private static final Logger logger = LoggerFactory.getLogger(ListenerProxy.class);
+
+ public enum Kind { COMMAND, COMMANDS_FOR_KEY }
+
+ public abstract Kind kind();
+ public abstract ByteBuffer identifier();
+
+ final CommandStore commandStore;
+
+ private ListenerProxy(CommandStore commandStore)
+ {
+ this.commandStore = commandStore;
+ }
+
+ @Override
+ public int compareTo(ListenerProxy that)
+ {
+ return kind().compareTo(that.kind());
+ }
+
+ protected abstract long estimatedSizeOnHeap();
+
+ static class CommandListenerProxy extends ListenerProxy
+ {
+ private static final long EMPTY_SIZE = ObjectSizes.measure(new CommandListenerProxy(null, null));
+ private final TxnId txnId;
+
+ public CommandListenerProxy(CommandStore commandStore, TxnId txnId)
+ {
+ super(commandStore);
+ this.txnId = txnId;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CommandListenerProxy that = (CommandListenerProxy) o;
+ return txnId.equals(that.txnId);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(txnId);
+ }
+
+ @Override
+ public int compareTo(ListenerProxy that)
+ {
+ int cmp = super.compareTo(that);
+ if (cmp != 0)
+ return cmp;
+
+ return this.txnId.compareTo(((CommandListenerProxy) that).txnId);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "CommandListenerProxy{" +
+ "txnId=" + txnId +
+ '}';
+ }
+
+ @Override
+ public PreLoadContext listenerPreLoadContext(TxnId caller)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Kind kind()
+ {
+ return Kind.COMMAND;
+ }
+
+ @Override
+ public ByteBuffer identifier()
+ {
+ ByteBuffer bytes = ByteBuffer.allocate(1 + CommandSerializers.txnId.serializedSize());
+ ByteBufferAccessor.instance.putByte(bytes, 0, (byte) kind().ordinal());
+ CommandSerializers.txnId.serialize(txnId, bytes, ByteBufferAccessor.instance, 1);
+ return bytes;
+ }
+
+ @Override
+ public void onChange(Command c)
+ {
+ AccordCommand command = (AccordCommand) c;
+ AccordCommandStore commandStore = command.commandStore();
+ AsyncContext context = commandStore.getContext();
+ PreLoadContext loadCtx = PreLoadContext.contextFor(ImmutableList.of(command.txnId(), txnId), Collections.emptyList());
+ if (context.containsScopedItems(loadCtx))
+ {
+ // TODO (soon): determine if this can break anything by not waiting for the current operation to denormalize it's data
+ // the summary loader may default to commands in context, in case it wouldn't
+ logger.trace("{}: synchronously updating listening command {}", c.txnId(), txnId);
+ commandStore.command(txnId).onChange(c);
+ }
+ else
+ {
+ TxnId callingTxnId = command.txnId();
+ logger.trace("{}: asynchronously updating listening command {}", c.txnId(), txnId);
+ commandStore.process(loadCtx, instance -> {
+ Command caller = instance.command(callingTxnId);
+ commandStore.command(txnId).onChange(caller);
+ });
+ }
+ }
+
+ @Override
+ protected long estimatedSizeOnHeap()
+ {
+ return EMPTY_SIZE + AccordObjectSizes.timestamp(txnId);
+ }
+ }
+
+ /**
+ * These always need to be run in either the same task as the notifying command, or immediately afterwards, otherwise we
+ * may use stale max timestamps for preaccept
+ */
+ static class CommandsForKeyListenerProxy extends ListenerProxy
+ {
+ private static final long EMPTY_SIZE = ObjectSizes.measure(new CommandsForKeyListenerProxy(null, null));
+ private final AccordKey.PartitionKey key;
+
+ public CommandsForKeyListenerProxy(CommandStore commandStore, AccordKey.PartitionKey key)
+ {
+ super(commandStore);
+ this.key = key;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CommandsForKeyListenerProxy that = (CommandsForKeyListenerProxy) o;
+ return key.equals(that.key);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(key);
+ }
+
+ @Override
+ public int compareTo(ListenerProxy that)
+ {
+ int cmp = super.compareTo(that);
+ if (cmp != 0)
+ return cmp;
+
+ return this.key.compareTo(((CommandsForKeyListenerProxy) that).key);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "CommandsForKeyListenerProxy{" +
+ "key=" + key +
+ '}';
+ }
+
+ @Override
+ public PreLoadContext listenerPreLoadContext(TxnId caller)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Kind kind()
+ {
+ return Kind.COMMANDS_FOR_KEY;
+ }
+
+ @Override
+ public ByteBuffer identifier()
+ {
+ ByteBuffer bytes = ByteBuffer.allocate((int) (1 + AccordKey.PartitionKey.serializer.serializedSize(key)));
+ ByteBufferAccessor.instance.putByte(bytes, 0, (byte) kind().ordinal());
+ AccordKey.PartitionKey.serializer.serialize(key, bytes, ByteBufferAccessor.instance, 1);
+ return bytes;
+ }
+
+ @Override
+ public void onChange(Command c)
+ {
+ AccordCommand command = (AccordCommand) c;
+ AccordCommandStore commandStore = command.commandStore();
+ AsyncContext context = commandStore.getContext();
+ PreLoadContext loadCtx = PreLoadContext.contextFor(ImmutableList.of(command.txnId()), ImmutableList.of(key));
+ if (context.containsScopedItems(loadCtx))
+ {
+ logger.trace("{}: synchronously updating listening cfk {}", c.txnId(), key);
+ commandStore.commandsForKey(key).onChange(c);
+ }
+ else
+ {
+ TxnId callingTxnId = command.txnId();
+ logger.trace("{}: asynchronously updating listening cfk {}", c.txnId(), key);
+ commandStore.process(loadCtx, instance -> {
+ Command caller = instance.command(callingTxnId);
+ commandStore.commandsForKey(key).onChange(caller);
+ });
+ }
+ }
+
+ @Override
+ protected long estimatedSizeOnHeap()
+ {
+ return EMPTY_SIZE + key.estimatedSizeOnHeap();
+ }
+ }
+
+ public static <V> ListenerProxy deserialize(CommandStore commandStore, V src, ValueAccessor<V> accessor, int offset) throws IOException
+ {
+ int ordinal = accessor.getByte(src, offset);
+ Kind kind = Kind.values()[ordinal];
+ offset += 1;
+ switch (kind)
+ {
+ case COMMAND:
+ TxnId txnId = CommandSerializers.txnId.deserialize(src, accessor, offset);
+ return new CommandListenerProxy(commandStore, txnId);
+ case COMMANDS_FOR_KEY:
+ AccordKey.PartitionKey key = AccordKey.PartitionKey.serializer.deserialize(src, accessor, offset);
+ return new CommandsForKeyListenerProxy(commandStore, key);
+ default:
+ throw new IOException("Unknown kind ordinal " + ordinal);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/ReadFuture.java b/src/java/org/apache/cassandra/service/accord/ReadFuture.java
new file mode 100644
index 0000000000..46eb4f86ad
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/ReadFuture.java
@@ -0,0 +1,295 @@
+/*
+ * 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.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+import accord.api.Data;
+import accord.primitives.Keys;
+import io.netty.util.concurrent.GenericFutureListener;
+import org.apache.cassandra.utils.concurrent.Future;
+import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
+
+public class ReadFuture implements Future<Data>
+{
+ final Keys scope;
+ private final Future<Data> wrappped;
+
+ public ReadFuture(Keys scope, Future<Data> wrappped)
+ {
+ this.scope = scope;
+ this.wrappped = wrappped;
+ }
+
+ @Override
+ public Future<Data> await() throws InterruptedException
+ {
+ return wrappped.await();
+ }
+
+ @Override
+ public Future<Data> awaitUninterruptibly()
+ {
+ return wrappped.awaitUninterruptibly();
+ }
+
+ @Override
+ public Future<Data> awaitThrowUncheckedOnInterrupt()
+ {
+ return wrappped.awaitThrowUncheckedOnInterrupt();
+ }
+
+ @Override
+ public void rethrowIfFailed()
+ {
+ wrappped.rethrowIfFailed();
+ }
+
+ @Override
+ public Future<Data> sync() throws InterruptedException
+ {
+ return wrappped.sync();
+ }
+
+ @Override
+ public Future<Data> syncUninterruptibly()
+ {
+ return wrappped.syncUninterruptibly();
+ }
+
+ @Override
+ public Future<Data> syncThrowUncheckedOnInterrupt()
+ {
+ return wrappped.syncThrowUncheckedOnInterrupt();
+ }
+
+ @Override
+ @Deprecated
+ public boolean await(long l) throws InterruptedException
+ {
+ return wrappped.await(l);
+ }
+
+ @Override
+ @Deprecated
+ public boolean awaitUninterruptibly(long l)
+ {
+ return wrappped.awaitUninterruptibly(l);
+ }
+
+ @Override
+ public Future<Data> addCallback(BiConsumer<? super Data, Throwable> callback)
+ {
+ return wrappped.addCallback(callback);
+ }
+
+ @Override
+ public Future<Data> addCallback(BiConsumer<? super Data, Throwable> callback, Executor executor)
+ {
+ return wrappped.addCallback(callback, executor);
+ }
+
+ @Override
+ public Future<Data> addCallback(FutureCallback<? super Data> callback)
+ {
+ return wrappped.addCallback(callback);
+ }
+
+ @Override
+ public Future<Data> addCallback(FutureCallback<? super Data> callback, Executor executor)
+ {
+ return wrappped.addCallback(callback, executor);
+ }
+
+ @Override
+ public Future<Data> addCallback(Consumer<? super Data> onSuccess, Consumer<? super Throwable> onFailure)
+ {
+ return wrappped.addCallback(onSuccess, onFailure);
+ }
+
+ @Override
+ public Future<Data> addCallback(Consumer<? super Data> onSuccess, Consumer<? super Throwable> onFailure, Executor executor)
+ {
+ return wrappped.addCallback(onSuccess, onFailure, executor);
+ }
+
+ @Override
+ public <T> Future<T> map(Function<? super Data, ? extends T> mapper)
+ {
+ return wrappped.map(mapper);
+ }
+
+ @Override
+ public <T> Future<T> map(Function<? super Data, ? extends T> mapper, Executor executor)
+ {
+ return wrappped.map(mapper, executor);
+ }
+
+ @Override
+ public <T> Future<T> flatMap(Function<? super Data, ? extends Future<T>> flatMapper)
+ {
+ return wrappped.flatMap(flatMapper);
+ }
+
+ @Override
+ public <T> Future<T> flatMap(Function<? super Data, ? extends Future<T>> flatMapper, Executor executor)
+ {
+ return wrappped.flatMap(flatMapper, executor);
+ }
+
+ @Override
+ public void addListener(Runnable runnable, Executor executor)
+ {
+ wrappped.addListener(runnable, executor);
+ }
+
+ @Override
+ public void addListener(Runnable runnable)
+ {
+ wrappped.addListener(runnable);
+ }
+
+ @Override
+ public Executor notifyExecutor()
+ {
+ return wrappped.notifyExecutor();
+ }
+
+ @Override
+ public Future<Data> addListener(GenericFutureListener<? extends io.netty.util.concurrent.Future<? super Data>> genericFutureListener)
+ {
+ return wrappped.addListener(genericFutureListener);
+ }
+
+ @Override
+ public Future<Data> addListeners(GenericFutureListener<? extends io.netty.util.concurrent.Future<? super Data>>... genericFutureListeners)
+ {
+ return wrappped.addListeners(genericFutureListeners);
+ }
+
+ @Override
+ public Future<Data> removeListener(GenericFutureListener<? extends io.netty.util.concurrent.Future<? super Data>> genericFutureListener)
+ {
+ return wrappped.removeListener(genericFutureListener);
+ }
+
+ @Override
+ public Future<Data> removeListeners(GenericFutureListener<? extends io.netty.util.concurrent.Future<? super Data>>... genericFutureListeners)
+ {
+ return wrappped.removeListeners(genericFutureListeners);
+ }
+
+ @Override
+ public boolean isSuccess()
+ {
+ return wrappped.isSuccess();
+ }
+
+ @Override
+ public boolean isCancellable()
+ {
+ return wrappped.isCancellable();
+ }
+
+ @Override
+ public Throwable cause()
+ {
+ return wrappped.cause();
+ }
+
+ @Override
+ public boolean await(long timeout, TimeUnit unit) throws InterruptedException
+ {
+ return wrappped.await(timeout, unit);
+ }
+
+ @Override
+ public boolean awaitUninterruptibly(long timeout, TimeUnit unit)
+ {
+ return wrappped.awaitUninterruptibly(timeout, unit);
+ }
+
+ @Override
+ public Data getNow()
+ {
+ return wrappped.getNow();
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning)
+ {
+ return wrappped.cancel(mayInterruptIfRunning);
+ }
+
+ @Override
+ public boolean isCancelled()
+ {
+ return wrappped.isCancelled();
+ }
+
+ @Override
+ public boolean isDone()
+ {
+ return wrappped.isDone();
+ }
+
+ @Override
+ public Data get() throws InterruptedException, ExecutionException
+ {
+ return wrappped.get();
+ }
+
+ @Override
+ public Data get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
+ {
+ return wrappped.get(timeout, unit);
+ }
+
+ @Override
+ public boolean awaitUntil(long nanoTimeDeadline) throws InterruptedException
+ {
+ return wrappped.awaitUntil(nanoTimeDeadline);
+ }
+
+ @Override
+ public boolean awaitUntilThrowUncheckedOnInterrupt(long nanoTimeDeadline) throws UncheckedInterruptedException
+ {
+ return wrappped.awaitUntilThrowUncheckedOnInterrupt(nanoTimeDeadline);
+ }
+
+ @Override
+ public boolean awaitUntilUninterruptibly(long nanoTimeDeadline)
+ {
+ return wrappped.awaitUntilUninterruptibly(nanoTimeDeadline);
+ }
+
+ @Override
+ public boolean awaitThrowUncheckedOnInterrupt(long time, TimeUnit units) throws UncheckedInterruptedException
+ {
+ return wrappped.awaitThrowUncheckedOnInterrupt(time, units);
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/TokenRange.java b/src/java/org/apache/cassandra/service/accord/TokenRange.java
new file mode 100644
index 0000000000..0f8d7d5293
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/TokenRange.java
@@ -0,0 +1,86 @@
+/*
+ * 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 com.google.common.base.Preconditions;
+
+import accord.api.Key;
+import accord.primitives.KeyRange;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.api.AccordKey.SentinelKey;
+import org.apache.cassandra.service.accord.api.AccordKey.TokenKey;
+
+public class TokenRange extends KeyRange.EndInclusive
+{
+ public TokenRange(AccordKey start, AccordKey end)
+ {
+ super(start, end);
+ Preconditions.checkArgument(start.kind().supportsRanges());
+ Preconditions.checkArgument(end.kind().supportsRanges());
+ }
+
+ private static AccordKey toAccordTokenOrSentinel(Key key)
+ {
+ AccordKey aKey = (AccordKey) key;
+ if (aKey instanceof TokenKey || aKey instanceof SentinelKey)
+ return aKey;
+ return new TokenKey(aKey.tableId(), aKey.partitionKey().getToken().maxKeyBound());
+ }
+
+ public static TokenRange fullRange(TableId tableId)
+ {
+ return new TokenRange(SentinelKey.min(tableId), SentinelKey.max(tableId));
+ }
+
+ @Override
+ public TokenRange subRange(Key start, Key end)
+ {
+ return new TokenRange(toAccordTokenOrSentinel(start), toAccordTokenOrSentinel(end));
+ }
+
+ public static final IVersionedSerializer<TokenRange> serializer = new IVersionedSerializer<TokenRange>()
+ {
+ @Override
+ public void serialize(TokenRange range, DataOutputPlus out, int version) throws IOException
+ {
+ AccordKey.serializer.serialize((AccordKey) range.start(), out, version);
+ AccordKey.serializer.serialize((AccordKey) range.end(), out, version);
+ }
+
+ @Override
+ public TokenRange deserialize(DataInputPlus in, int version) throws IOException
+ {
+ return new TokenRange(AccordKey.serializer.deserialize(in, version),
+ AccordKey.serializer.deserialize(in, version));
+ }
+
+ @Override
+ public long serializedSize(TokenRange range, int version)
+ {
+ return AccordKey.serializer.serializedSize((AccordKey) range.start(), version)
+ + AccordKey.serializer.serializedSize((AccordKey) range.end(), version);
+ }
+ };
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/shared/VersionedApplicationState.java b/src/java/org/apache/cassandra/service/accord/api/AccordAgent.java
similarity index 59%
copy from test/distributed/org/apache/cassandra/distributed/shared/VersionedApplicationState.java
copy to src/java/org/apache/cassandra/service/accord/api/AccordAgent.java
index fd3e40a70d..a5e94d550f 100644
--- a/test/distributed/org/apache/cassandra/distributed/shared/VersionedApplicationState.java
+++ b/src/java/org/apache/cassandra/service/accord/api/AccordAgent.java
@@ -16,20 +16,31 @@
* limitations under the License.
*/
-package org.apache.cassandra.distributed.shared;
+package org.apache.cassandra.service.accord.api;
-import java.io.Serializable;
+import accord.api.Agent;
+import accord.api.Result;
+import accord.local.Command;
+import accord.local.Node;
+import accord.primitives.Timestamp;
-public class VersionedApplicationState implements Serializable
+public class AccordAgent implements Agent
{
- public final int applicationState;
- public final String value;
- public final int version;
+ @Override
+ public void onRecover(Node node, Result success, Throwable fail)
+ {
+ // TODO: this
+ }
+
+ @Override
+ public void onInconsistentTimestamp(Command command, Timestamp prev, Timestamp next)
+ {
+ // TODO: this
+ }
- public VersionedApplicationState(int applicationState, String value, int version)
+ @Override
+ public void onUncaughtException(Throwable t)
{
- this.applicationState = applicationState;
- this.value = value;
- this.version = version;
+ // TODO: this
}
}
diff --git a/src/java/org/apache/cassandra/service/accord/api/AccordKey.java b/src/java/org/apache/cassandra/service/accord/api/AccordKey.java
new file mode 100644
index 0000000000..845d43cec0
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/api/AccordKey.java
@@ -0,0 +1,480 @@
+/*
+ * 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.api;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+import com.google.common.base.Preconditions;
+
+import accord.api.Key;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.db.SinglePartitionReadCommand;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.db.marshal.ByteBufferAccessor;
+import org.apache.cassandra.db.marshal.ValueAccessor;
+import org.apache.cassandra.db.partitions.Partition;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.ObjectSizes;
+
+public interface AccordKey extends Key
+{
+ enum Kind
+ {
+ TOKEN, PARTITION, SENTINEL;
+
+ public boolean supportsRanges()
+ {
+ switch (this)
+ {
+ case SENTINEL:
+ case TOKEN:
+ return true;
+ default:
+ return false;
+ }
+ }
+ };
+
+ TableId tableId();
+ PartitionPosition partitionKey();
+ Kind kind();
+
+ static AccordKey of(Key key)
+ {
+ return (AccordKey) key;
+ }
+
+ static PartitionKey of(Partition partition)
+ {
+ return new PartitionKey(partition.metadata().id, partition.partitionKey());
+ }
+
+ static PartitionKey of(SinglePartitionReadCommand command)
+ {
+ return new PartitionKey(command.metadata().id, command.partitionKey());
+ }
+
+ static int compare(AccordKey left, AccordKey right)
+ {
+ int cmp = left.tableId().compareTo(right.tableId());
+ if (cmp != 0)
+ return cmp;
+
+ if (left instanceof SentinelKey || right instanceof SentinelKey)
+ {
+ int leftInt = left instanceof SentinelKey ? ((SentinelKey) left).asInt() : 0;
+ int rightInt = right instanceof SentinelKey ? ((SentinelKey) right).asInt() : 0;
+ return Integer.compare(leftInt, rightInt);
+ }
+
+ return left.partitionKey().compareTo(right.partitionKey());
+ }
+
+ static int compareKeys(Key left, Key right)
+ {
+ return compare((AccordKey) left, (AccordKey) right);
+ }
+
+ default int compareTo(AccordKey that)
+ {
+ return compare(this, that);
+ }
+
+ @Override
+ default int routingHash()
+ {
+ return partitionKey().getToken().tokenHash();
+ }
+
+ class SentinelKey implements AccordKey
+ {
+ private final TableId tableId;
+ private final boolean isMin;
+
+ private SentinelKey(TableId tableId, boolean isMin)
+ {
+ this.tableId = tableId;
+ this.isMin = isMin;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SentinelKey that = (SentinelKey) o;
+ return isMin == that.isMin && tableId.equals(that.tableId);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(tableId, isMin);
+ }
+
+ @Override
+ public int compareTo(Key that)
+ {
+ return compare(this, (AccordKey) that);
+ }
+
+ @Override
+ public Kind kind()
+ {
+ return Kind.SENTINEL;
+ }
+
+ public static SentinelKey min(TableId tableId)
+ {
+ return new SentinelKey(tableId, true);
+ }
+
+ public static SentinelKey max(TableId tableId)
+ {
+ return new SentinelKey(tableId, false);
+ }
+
+ @Override
+ public TableId tableId()
+ {
+ return tableId;
+ }
+
+ @Override
+ public PartitionPosition partitionKey()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ int asInt()
+ {
+ return isMin ? -1 : 1;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "SentinelKey{" +
+ "tableId=" + tableId +
+ ", key=" + (isMin ? "min": "max") +
+ '}';
+ }
+
+ public static final IVersionedSerializer<SentinelKey> serializer = new IVersionedSerializer<SentinelKey>()
+ {
+ @Override
+ public void serialize(SentinelKey key, DataOutputPlus out, int version) throws IOException
+ {
+ out.writeBoolean(key.isMin);
+ key.tableId().serialize(out);
+ }
+
+ @Override
+ public SentinelKey deserialize(DataInputPlus in, int version) throws IOException
+ {
+ boolean isMin = in.readBoolean();
+ TableId tableId = TableId.deserialize(in);
+ return new SentinelKey(tableId, isMin);
+ }
+
+ @Override
+ public long serializedSize(SentinelKey key, int version)
+ {
+ return TypeSizes.BOOL_SIZE + TableId.serializedSize();
+ }
+ };
+ }
+
+ abstract class AbstractKey<T extends PartitionPosition> implements AccordKey
+ {
+ private final TableId tableId;
+ private final T key;
+
+ public AbstractKey(TableId tableId, T key)
+ {
+ this.tableId = tableId;
+ this.key = key;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AbstractKey<?> that = (AbstractKey<?>) o;
+ return tableId.equals(that.tableId) && key.equals(that.key);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(tableId, key);
+ }
+
+ @Override
+ public int compareTo(Key that)
+ {
+ return compare(this, (AccordKey) that);
+ }
+
+ @Override
+ public TableId tableId()
+ {
+ return tableId;
+ }
+
+ @Override
+ public T partitionKey()
+ {
+ return key;
+ }
+ }
+
+ class PartitionKey extends AbstractKey<DecoratedKey>
+ {
+ private static final long EMPTY_SIZE;
+ static
+ {
+ DecoratedKey key = DatabaseDescriptor.getPartitioner().decorateKey(ByteBufferUtil.EMPTY_BYTE_BUFFER);
+ EMPTY_SIZE = ObjectSizes.measureDeep(new PartitionKey(null, key));
+ }
+
+ public PartitionKey(TableId tableId, DecoratedKey key)
+ {
+ super(tableId, key);
+ }
+
+ @Override
+ public Kind kind()
+ {
+ return Kind.PARTITION;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "PartitionKey{" +
+ "tableId=" + tableId() +
+ ", key=" + partitionKey() +
+ '}';
+ }
+
+ public long estimatedSizeOnHeap()
+ {
+ return EMPTY_SIZE + ByteBufferAccessor.instance.size(partitionKey().getKey());
+ }
+
+ public static final Serializer serializer = new Serializer();
+ public static class Serializer implements IVersionedSerializer<PartitionKey>
+ {
+ // TODO: add vint to value accessor and use vints
+ private Serializer() {}
+
+ @Override
+ public void serialize(PartitionKey key, DataOutputPlus out, int version) throws IOException
+ {
+ key.tableId().serialize(out);
+ ByteBufferUtil.writeWithShortLength(key.partitionKey().getKey(), out);
+ }
+
+ public <V> int serialize(PartitionKey key, V dst, ValueAccessor<V> accessor, int offset)
+ {
+ int position = offset;
+ position += key.tableId().serialize(dst, accessor, position);
+ ByteBuffer bytes = key.partitionKey().getKey();
+ int numBytes = ByteBufferAccessor.instance.size(bytes);
+ Preconditions.checkState(numBytes <= Short.MAX_VALUE);
+ position += accessor.putShort(dst, position, (short) numBytes);
+ position += accessor.copyByteBufferTo(bytes, 0, dst, position, numBytes);
+ return position - offset;
+
+ }
+
+ @Override
+ public PartitionKey deserialize(DataInputPlus in, int version) throws IOException
+ {
+ TableId tableId = TableId.deserialize(in);
+ TableMetadata metadata = Schema.instance.getTableMetadata(tableId);
+ DecoratedKey key = metadata.partitioner.decorateKey(ByteBufferUtil.readWithShortLength(in));
+ return new PartitionKey(tableId, key);
+ }
+
+ public <V> PartitionKey deserialize(V src, ValueAccessor<V> accessor, int offset) throws IOException
+ {
+ TableId tableId = TableId.deserialize(src, accessor, offset);
+ offset += TableId.serializedSize();
+ TableMetadata metadata = Schema.instance.getTableMetadata(tableId);
+ int numBytes = accessor.getShort(src, offset);
+ offset += TypeSizes.SHORT_SIZE;
+ ByteBuffer bytes = ByteBuffer.allocate(numBytes);
+ accessor.copyTo(src, offset, bytes, ByteBufferAccessor.instance, 0, numBytes);
+ DecoratedKey key = metadata.partitioner.decorateKey(bytes);
+ return new PartitionKey(tableId, key);
+ }
+
+ @Override
+ public long serializedSize(PartitionKey key, int version)
+ {
+ return serializedSize(key);
+ }
+
+ public long serializedSize(PartitionKey key)
+ {
+ return key.tableId().serializedSize() + ByteBufferUtil.serializedSizeWithShortLength(key.partitionKey().getKey());
+ }
+ }
+ }
+
+ class TokenKey extends AbstractKey<Token.KeyBound>
+ {
+ public TokenKey(TableId tableId, Token.KeyBound key)
+ {
+ super(tableId, key);
+ }
+
+ public static TokenKey min(TableId tableId, Token token)
+ {
+ return new TokenKey(tableId, token.minKeyBound());
+ }
+
+ public static TokenKey max(TableId tableId, Token token)
+ {
+ return new TokenKey(tableId, token.maxKeyBound());
+ }
+
+ @Override
+ public Kind kind()
+ {
+ return Kind.TOKEN;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "TokenKey{" +
+ "tableId=" + tableId() +
+ ", key=" + partitionKey() +
+ '}';
+ }
+
+ public static final IVersionedSerializer<TokenKey> serializer = new IVersionedSerializer<TokenKey>()
+ {
+ @Override
+ public void serialize(TokenKey key, DataOutputPlus out, int version) throws IOException
+ {
+ key.tableId().serialize(out);
+ Token.KeyBound bound = key.partitionKey();
+ out.writeBoolean(bound.isMinimumBound);
+ Token.serializer.serialize(bound.getToken(), out, version);
+ }
+
+ @Override
+ public TokenKey deserialize(DataInputPlus in, int version) throws IOException
+ {
+ TableId tableId = TableId.deserialize(in);
+ TableMetadata metadata = Schema.instance.getTableMetadata(tableId);
+ boolean isMinimumBound = in.readBoolean();
+ Token token = Token.serializer.deserialize(in, metadata.partitioner, version);
+ return new TokenKey(tableId, isMinimumBound ? token.minKeyBound() : token.maxKeyBound());
+ }
+
+ @Override
+ public long serializedSize(TokenKey key, int version)
+ {
+ Token.KeyBound bound = key.partitionKey();
+ return key.tableId().serializedSize()
+ + TypeSizes.sizeof(bound.isMinimumBound)
+ + Token.serializer.serializedSize(bound.getToken(), version);
+ }
+ };
+ }
+
+ IVersionedSerializer<AccordKey> serializer = new IVersionedSerializer<AccordKey>()
+ {
+ @Override
+ public void serialize(AccordKey key, DataOutputPlus out, int version) throws IOException
+ {
+ out.write(key.kind().ordinal());
+ switch (key.kind())
+ {
+ case TOKEN:
+ TokenKey.serializer.serialize((TokenKey) key, out, version);
+ break;
+ case PARTITION:
+ PartitionKey.serializer.serialize((PartitionKey) key, out, version);
+ break;
+ case SENTINEL:
+ SentinelKey.serializer.serialize((SentinelKey) key, out, version);
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+
+ }
+
+ @Override
+ public AccordKey deserialize(DataInputPlus in, int version) throws IOException
+ {
+ Kind kind = Kind.values()[in.readByte()];
+ switch (kind)
+ {
+ case TOKEN:
+ return TokenKey.serializer.deserialize(in, version);
+ case PARTITION:
+ return PartitionKey.serializer.deserialize(in, version);
+ case SENTINEL:
+ return SentinelKey.serializer.deserialize(in, version);
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ @Override
+ public long serializedSize(AccordKey key, int version)
+ {
+ long size = TypeSizes.BYTE_SIZE; // kind ordinal
+ switch (key.kind())
+ {
+ case TOKEN:
+ size += TokenKey.serializer.serializedSize((TokenKey) key, version);
+ break;
+ case PARTITION:
+ size += PartitionKey.serializer.serializedSize((PartitionKey) key, version);
+ break;
+ case SENTINEL:
+ size += SentinelKey.serializer.serializedSize((SentinelKey) key, version);
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ return size;
+ }
+ };
+}
diff --git a/src/java/org/apache/cassandra/service/accord/api/AccordScheduler.java b/src/java/org/apache/cassandra/service/accord/api/AccordScheduler.java
new file mode 100644
index 0000000000..df83a92932
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/api/AccordScheduler.java
@@ -0,0 +1,97 @@
+/*
+ * 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.api;
+
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import accord.api.Scheduler;
+import org.apache.cassandra.concurrent.ExecutorFactory;
+import org.apache.cassandra.concurrent.ScheduledExecutorPlus;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+public class AccordScheduler implements Scheduler, Shutdownable
+{
+ private final ScheduledExecutorPlus scheduledExecutor = ExecutorFactory.Global.executorFactory().scheduled("AccordScheduled");
+
+ private static class ScheduledFutureWrapper implements Scheduled
+ {
+ private final ScheduledFuture<?> future;
+
+ public ScheduledFutureWrapper(ScheduledFuture<?> future)
+ {
+ this.future = future;
+ }
+
+ @Override
+ public void cancel()
+ {
+ future.cancel(false);
+ }
+ }
+
+ @Override
+ public Scheduled recurring(Runnable run, long delay, TimeUnit units)
+ {
+ ScheduledFuture<?> future = scheduledExecutor.scheduleAtFixedRate(run, delay, delay, units);
+ return new ScheduledFutureWrapper(future);
+ }
+
+ @Override
+ public Scheduled once(Runnable run, long delay, TimeUnit units)
+ {
+ ScheduledFuture<?> future = scheduledExecutor.schedule(run, delay, units);
+ return new ScheduledFutureWrapper(future);
+ }
+
+ @Override
+ public void now(Runnable task)
+ {
+ // called from the mutation stage configured by the verb
+ if (scheduledExecutor.isShutdown())
+ throw new RejectedExecutionException("Scheduler has shut down.");
+ scheduledExecutor.submit(task);
+ }
+
+ @Override
+ public boolean isTerminated()
+ {
+ return scheduledExecutor.isTerminated();
+ }
+
+ @Override
+ public void shutdown()
+ {
+ scheduledExecutor.shutdown();
+ }
+
+ @Override
+ public List<Runnable> shutdownNow()
+ {
+ return scheduledExecutor.shutdownNow();
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException
+ {
+ return scheduledExecutor.awaitTermination(timeout, units);
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/async/AsyncContext.java b/src/java/org/apache/cassandra/service/accord/async/AsyncContext.java
new file mode 100644
index 0000000000..0b3c87536f
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/async/AsyncContext.java
@@ -0,0 +1,116 @@
+/*
+ * 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.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+
+import accord.local.PreLoadContext;
+import accord.primitives.TxnId;
+import org.apache.cassandra.service.accord.AccordCommand;
+import org.apache.cassandra.service.accord.AccordCommandStore;
+import org.apache.cassandra.service.accord.AccordCommandsForKey;
+import org.apache.cassandra.service.accord.AccordPartialCommand;
+import org.apache.cassandra.service.accord.AccordState;
+import org.apache.cassandra.service.accord.AccordStateCache;
+import org.apache.cassandra.service.accord.AccordState.WriteOnly;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+
+public class AsyncContext
+{
+ public static class Group<K, V extends AccordState<K>>
+ {
+ final Map<K, V> items = new HashMap<>();
+ final Map<K, WriteOnly<K, V>> writeOnly = new HashMap<>();
+
+ @VisibleForTesting
+ public void add(V item)
+ {
+ items.put(item.key(), item);
+ }
+
+ public V get(K key)
+ {
+ return items.get(key);
+ }
+
+ void releaseResources(AccordStateCache.Instance<K, V> cache)
+ {
+ items.values().forEach(cache::release);
+ items.clear();
+ writeOnly.clear();
+ }
+
+ public WriteOnly<K, V> getOrCreateWriteOnly(K key, BiFunction<AccordCommandStore, K, WriteOnly<K, V>> factory, AccordCommandStore commandStore)
+ {
+ Preconditions.checkState(!items.containsKey(key));
+ WriteOnly<K, V> command = writeOnly.get(key);
+ if (command == null)
+ {
+ command = factory.apply(commandStore, key);
+ writeOnly.put(key, command);
+ }
+ return command;
+ }
+ }
+
+ public static class CommandGroup extends Group<TxnId, AccordCommand>
+ {
+ List<AccordPartialCommand> partials = new ArrayList<>();
+
+ public void addPartialCommand(AccordPartialCommand partial)
+ {
+ partials.add(partial);
+ }
+
+ @Override
+ void releaseResources(AccordStateCache.Instance<TxnId, AccordCommand> cache)
+ {
+ super.releaseResources(cache);
+ partials.clear();
+ }
+ }
+
+ public final CommandGroup commands = new CommandGroup();
+ public final Group<PartitionKey, AccordCommandsForKey> commandsForKey = new Group<>();
+
+ public boolean containsScopedItems(PreLoadContext loadContext)
+ {
+ return Iterables.all(loadContext.txnIds(), commands.items::containsKey) && Iterables.all(loadContext.keys(), commandsForKey.items::containsKey);
+ }
+
+ void verifyLoaded()
+ {
+ commands.items.forEach((key, command) -> Preconditions.checkState(command.isLoaded()));
+ commandsForKey.items.forEach((key, cfk) -> Preconditions.checkState(cfk.isLoaded()));
+ }
+
+ void releaseResources(AccordCommandStore commandStore)
+ {
+ commands.releaseResources(commandStore.commandCache());
+ commandsForKey.releaseResources(commandStore.commandsForKeyCache());
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/async/AsyncLoader.java b/src/java/org/apache/cassandra/service/accord/async/AsyncLoader.java
new file mode 100644
index 0000000000..2cdb099394
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/async/AsyncLoader.java
@@ -0,0 +1,240 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.primitives.TxnId;
+import org.apache.cassandra.concurrent.Stage;
+import org.apache.cassandra.service.accord.AccordCommand;
+import org.apache.cassandra.service.accord.AccordCommandStore;
+import org.apache.cassandra.service.accord.AccordCommandsForKey;
+import org.apache.cassandra.service.accord.AccordKeyspace;
+import org.apache.cassandra.service.accord.AccordStateCache;
+import org.apache.cassandra.service.accord.AccordState;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.utils.concurrent.Future;
+import org.apache.cassandra.utils.concurrent.FutureCombiner;
+
+
+public class AsyncLoader
+{
+ private static final Logger logger = LoggerFactory.getLogger(AsyncLoader.class);
+ enum State
+ {
+ INITIALIZED,
+ SETUP,
+ LOADING,
+ FINISHED
+ }
+
+ private State state = State.INITIALIZED;
+ private final AccordCommandStore commandStore;
+
+ private final Iterable<TxnId> txnIds;
+ private final Iterable<PartitionKey> keys;
+
+ protected Future<?> readFuture;
+
+ public AsyncLoader(AccordCommandStore commandStore, Iterable<TxnId> txnIds, Iterable<PartitionKey> keys)
+ {
+ this.commandStore = commandStore;
+ this.txnIds = txnIds;
+ this.keys = keys;
+ }
+
+ private <K, V extends AccordState<K>> Future<?> referenceAndDispatch(K key,
+ AccordStateCache.Instance<K, V> cache,
+ Map<K, V> context,
+ Function<V, Future<?>> readFunction,
+ Object callback)
+ {
+ V item;
+ Future<?> future = cache.getLoadFuture(key);
+ if (future != null)
+ {
+ // if a load future exists for this, it must be present in the cache
+ item = cache.getOrNull(key);
+ Preconditions.checkState(item != null);
+ context.put(key, item);
+ if (logger.isTraceEnabled())
+ logger.trace("Existing load future found for {} while loading for {}. ({})", item.key(), callback, item);
+ return future;
+ }
+
+ item = cache.getOrCreate(key);
+ context.put(key, item);
+ if (item.isLoaded())
+ {
+ if (logger.isTraceEnabled())
+ logger.trace("Cached item found for {} while loading for {}. ({})", item.key(), callback, item);
+ return null;
+ }
+
+ future = readFunction.apply(item);
+ cache.setLoadFuture(item.key(), future);
+ if (logger.isTraceEnabled())
+ logger.trace("Loading new item for {} while loading for {}. ({})", item.key(), callback, item);
+ return future;
+ }
+
+
+ private <K, V extends AccordState<K>> List<Future<?>> referenceAndDispatchReads(Iterable<K> keys,
+ AccordStateCache.Instance<K, V> cache,
+ Map<K, V> context,
+ Function<V, Future<?>> readFunction,
+ List<Future<?>> futures,
+ Object callback)
+ {
+ for (K key : keys)
+ {
+ Future<?> future = referenceAndDispatch(key, cache, context, readFunction, callback);
+ if (future == null)
+ continue;
+
+ if (futures == null)
+ futures = new ArrayList<>();
+
+ futures.add(future);
+ }
+
+ return futures;
+ }
+
+ @VisibleForTesting
+ Function<AccordCommand, Future<?>> loadCommandFunction(Object callback)
+ {
+ return command -> Stage.READ.submit(() -> {
+ try
+ {
+ logger.trace("Starting load of {} for {}", command.txnId(), callback);
+ AccordKeyspace.loadCommand(command);
+ logger.trace("Completed load of {} for {}", command.txnId(), callback);
+ }
+ catch (Throwable t)
+ {
+ logger.error(String.format("Exception loading %s for %s", command.txnId(), callback), t);
+ throw t;
+ }
+ });
+ }
+
+ @VisibleForTesting
+ Function<AccordCommandsForKey, Future<?>> loadCommandsPerKeyFunction(Object callback)
+ {
+ return cfk -> Stage.READ.submit(() -> {
+ try
+ {
+ logger.trace("Starting load of {} for {}", cfk.key(), callback);
+ AccordKeyspace.loadCommandsForKey(cfk);
+ logger.trace("Completed load of {} for {}", cfk.key(), callback);
+ }
+ catch (Throwable t)
+ {
+ logger.error(String.format("Exception loading %s for %s", cfk.key(), callback), t);
+ throw t;
+ }
+ });
+ }
+
+ private Future<?> referenceAndDispatchReads(AsyncContext context, Object callback)
+ {
+ List<Future<?>> futures = null;
+
+ futures = referenceAndDispatchReads(txnIds,
+ commandStore.commandCache(),
+ context.commands.items,
+ loadCommandFunction(callback),
+ futures,
+ callback);
+
+ futures = referenceAndDispatchReads(keys,
+ commandStore.commandsForKeyCache(),
+ context.commandsForKey.items,
+ loadCommandsPerKeyFunction(callback),
+ futures,
+ callback);
+
+ return futures != null ? FutureCombiner.allOf(futures) : null;
+ }
+
+ @VisibleForTesting
+ void state(State state)
+ {
+ this.state = state;
+ }
+
+ public boolean load(AsyncContext context, BiConsumer<Object, Throwable> callback)
+ {
+ logger.trace("Running load for {} with state {}: {} {}", callback, state, txnIds, keys);
+ commandStore.checkInStoreThread();
+ switch (state)
+ {
+ case INITIALIZED:
+ state(State.SETUP);
+ case SETUP:
+ // notify any pending write only groups we're loading a full instance so the pending changes aren't removed
+ txnIds.forEach(commandStore.commandCache()::lockWriteOnlyGroupIfExists);
+ keys.forEach(commandStore.commandsForKeyCache()::lockWriteOnlyGroupIfExists);
+ readFuture = referenceAndDispatchReads(context, callback);
+ state(State.LOADING);
+ case LOADING:
+ if (readFuture != null)
+ {
+ if (readFuture.isSuccess())
+ {
+ logger.trace("Read future succeeded for {}", callback);
+ context.verifyLoaded();
+ readFuture = null;
+ }
+ else
+ {
+ logger.trace("Adding callback for read future: {}", callback);
+ readFuture.addCallback(callback, commandStore.executor());
+ break;
+ }
+ }
+ // apply any pending write only changes that may not have made it to disk in time to be loaded
+ context.commands.items.keySet().forEach(commandStore.commandCache()::cleanupLoadFuture);
+ context.commands.items.values().forEach(commandStore.commandCache()::applyAndRemoveWriteOnlyGroup);
+ context.commandsForKey.items.keySet().forEach(commandStore.commandsForKeyCache()::cleanupLoadFuture);
+ context.commandsForKey.items.values().forEach(commandStore.commandsForKeyCache()::applyAndRemoveWriteOnlyGroup);
+ // apply blindly reported timestamps
+ context.commandsForKey.items.values().forEach(AccordCommandsForKey::applyBlindWitnessedTimestamps);
+ state(State.FINISHED);
+ case FINISHED:
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+
+ logger.trace("Exiting load for {} with state {}: {} {}", callback, state, txnIds, keys);
+ return state == State.FINISHED;
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/async/AsyncOperation.java b/src/java/org/apache/cassandra/service/accord/async/AsyncOperation.java
new file mode 100644
index 0000000000..ca2ea8617d
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/async/AsyncOperation.java
@@ -0,0 +1,253 @@
+/*
+ * 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.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+
+import accord.api.Key;
+import accord.local.CommandStore;
+import accord.local.PreLoadContext;
+import accord.primitives.TxnId;
+import org.apache.cassandra.service.accord.AccordCommandStore;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.utils.concurrent.AsyncPromise;
+
+public abstract class AsyncOperation<R> extends AsyncPromise<R> implements Runnable, Function<CommandStore, R>, BiConsumer<Object, Throwable>
+{
+ private static final Logger logger = LoggerFactory.getLogger(AsyncOperation.class);
+
+ private static class LoggingProps
+ {
+ private static final String COMMAND_STORE = "command_store";
+ private static final String ASYNC_OPERATION = "async_op";
+ }
+
+ enum State
+ {
+ INITIALIZED,
+ LOADING,
+ RUNNING,
+ SAVING,
+ AWAITING_SAVE,
+ COMPLETING,
+ FINISHED,
+ FAILED
+ }
+
+ public interface Context
+ {
+
+ }
+
+ private State state = State.INITIALIZED;
+ private final AccordCommandStore commandStore;
+ private final AsyncLoader loader;
+ private final AsyncWriter writer;
+ private final AsyncContext context = new AsyncContext();
+ private R result;
+ private final String loggingId;
+
+ private void setLoggingIds()
+ {
+ MDC.put(LoggingProps.COMMAND_STORE, commandStore.loggingId);
+ MDC.put(LoggingProps.ASYNC_OPERATION, loggingId);
+ }
+
+ private void clearLoggingIds()
+ {
+ MDC.remove(LoggingProps.COMMAND_STORE);
+ MDC.remove(LoggingProps.ASYNC_OPERATION);
+ }
+
+ public AsyncOperation(AccordCommandStore commandStore, Iterable<TxnId> commandsToLoad, Iterable<PartitionKey> keyCommandsToLoad)
+ {
+ this.loggingId = "0x" + Integer.toHexString(System.identityHashCode(this));
+ this.commandStore = commandStore;
+ this.loader = createAsyncLoader(commandStore, commandsToLoad, keyCommandsToLoad);
+ setLoggingIds();
+ this.writer = createAsyncWriter(commandStore);
+ logger.trace("Created {} on {}", this, commandStore);
+ clearLoggingIds();
+ }
+
+ @Override
+ public String toString()
+ {
+ return "AsyncOperation{" + state + "}-0x" + Integer.toHexString(System.identityHashCode(this));
+ }
+
+ AsyncWriter createAsyncWriter(AccordCommandStore commandStore)
+ {
+ return new AsyncWriter(commandStore);
+ }
+
+ AsyncLoader createAsyncLoader(AccordCommandStore commandStore, Iterable<TxnId> txnIds, Iterable<PartitionKey> keys)
+ {
+ return new AsyncLoader(commandStore, txnIds, keys);
+ }
+
+ @VisibleForTesting
+ State state()
+ {
+ return state;
+ }
+
+ @VisibleForTesting
+ protected void setState(State state)
+ {
+ this.state = state;
+ }
+
+ /**
+ * callback for loader and writer
+ */
+ @Override
+ public void accept(Object o, Throwable throwable)
+ {
+ if (throwable != null)
+ {
+ logger.error(String.format("Operation %s failed", this), throwable);
+ state = State.FAILED;
+ tryFailure(throwable);
+ }
+ else
+ run();
+ }
+
+ protected void runInternal()
+ {
+ switch (state)
+ {
+ case INITIALIZED:
+ state = State.LOADING;
+ case LOADING:
+ if (!loader.load(context, this))
+ return;
+
+ state = State.RUNNING;
+ result = apply(commandStore);
+
+ state = State.SAVING;
+ case SAVING:
+ case AWAITING_SAVE:
+ boolean updatesPersisted = writer.save(context, this);
+
+ if (state != State.AWAITING_SAVE)
+ {
+ // with any updates on the way to disk, release resources so operations waiting
+ // to use these objects don't have issues with fields marked as unsaved
+ context.releaseResources(commandStore);
+ state = State.AWAITING_SAVE;
+ }
+
+ if (!updatesPersisted)
+ return;
+
+ state = State.COMPLETING;
+ setSuccess(result);
+ state = State.FINISHED;
+ case FINISHED:
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+
+ @Override
+ public void run()
+ {
+ setLoggingIds();
+ logger.trace("Running {} with state {}", this, state);
+ commandStore.checkInStoreThread();
+ commandStore.setContext(context);
+ try
+ {
+ runInternal();
+ }
+ catch (Throwable t)
+ {
+ logger.error(String.format("Operation %s failed", this), t);
+ tryFailure(t);
+ }
+ finally
+ {
+ commandStore.unsetContext(context);
+ logger.trace("Exiting {}", this);
+ clearLoggingIds();
+ }
+ }
+
+ private static Iterable<PartitionKey> toPartitionKeys(Iterable<? extends Key> iterable)
+ {
+ return (Iterable<PartitionKey>) iterable;
+ }
+
+ static class ForFunction<R> extends AsyncOperation<R>
+ {
+ private final Function<? super CommandStore, R> function;
+
+ public ForFunction(AccordCommandStore commandStore, Iterable<TxnId> txnIds, Iterable<PartitionKey> keys, Function<? super CommandStore, R> function)
+ {
+ super(commandStore, txnIds, keys);
+ this.function = function;
+ }
+
+ @Override
+ public R apply(CommandStore commandStore)
+ {
+ return function.apply(commandStore);
+ }
+ }
+
+ public static <T> AsyncOperation<T> create(CommandStore commandStore, PreLoadContext loadCtx, Function<? super CommandStore, T> function)
+ {
+ return new ForFunction<>((AccordCommandStore) commandStore, loadCtx.txnIds(), AsyncOperation.toPartitionKeys(loadCtx.keys()), function);
+ }
+
+ static class ForConsumer extends AsyncOperation<Void>
+ {
+ private final Consumer<? super CommandStore> consumer;
+
+ public ForConsumer(AccordCommandStore commandStore, Iterable<TxnId> txnIds, Iterable<PartitionKey> keys, Consumer<? super CommandStore> consumer)
+ {
+ super(commandStore, txnIds, keys);
+ this.consumer = consumer;
+ }
+
+ @Override
+ public Void apply(CommandStore commandStore)
+ {
+ consumer.accept(commandStore);
+ return null;
+ }
+ }
+
+ public static AsyncOperation<Void> create(CommandStore commandStore, PreLoadContext loadCtx, Consumer<? super CommandStore> consumer)
+ {
+ return new ForConsumer((AccordCommandStore) commandStore, loadCtx.txnIds(), AsyncOperation.toPartitionKeys(loadCtx.keys()), consumer);
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/async/AsyncWriter.java b/src/java/org/apache/cassandra/service/accord/async/AsyncWriter.java
new file mode 100644
index 0000000000..750aa93e12
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/async/AsyncWriter.java
@@ -0,0 +1,321 @@
+/*
+ * 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.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.api.Key;
+import accord.local.Status;
+import accord.primitives.TxnId;
+import org.apache.cassandra.concurrent.Stage;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.service.accord.AccordCommand;
+import org.apache.cassandra.service.accord.AccordCommandStore;
+import org.apache.cassandra.service.accord.AccordCommandsForKey;
+import org.apache.cassandra.service.accord.AccordKeyspace;
+import org.apache.cassandra.service.accord.AccordPartialCommand;
+import org.apache.cassandra.service.accord.AccordStateCache;
+import org.apache.cassandra.service.accord.AccordState;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.service.accord.store.StoredNavigableMap;
+import org.apache.cassandra.service.accord.store.StoredSet;
+import org.apache.cassandra.utils.concurrent.Future;
+import org.apache.cassandra.utils.concurrent.FutureCombiner;
+
+public class AsyncWriter
+{
+ private static final Logger logger = LoggerFactory.getLogger(AsyncWriter.class);
+
+ enum State
+ {
+ INITIALIZED,
+ SETUP,
+ SAVING,
+ FINISHED
+ }
+
+ private State state = State.INITIALIZED;
+ protected Future<?> writeFuture;
+ private final AccordCommandStore commandStore;
+ AccordStateCache.Instance<TxnId, AccordCommand> commandCache;
+ AccordStateCache.Instance<PartitionKey, AccordCommandsForKey> cfkCache;
+
+ public AsyncWriter(AccordCommandStore commandStore)
+ {
+ this.commandStore = commandStore;
+ this.commandCache = commandStore.commandCache();
+ this.cfkCache = commandStore.commandsForKeyCache();
+ }
+
+ private interface StateMutationFunction<K, V extends AccordState<K>>
+ {
+ Mutation apply(V state, long timestamp);
+ }
+
+ private static <K, V extends AccordState<K>> List<Future<?>> dispatchWrites(AsyncContext.Group<K, V> ctxGroup,
+ AccordStateCache.Instance<K, V> cache,
+ StateMutationFunction<K, V> mutationFunction,
+ long timestamp,
+ AccordCommandStore commandStore,
+ List<Future<?>> futures,
+ Object callback)
+ {
+ for (V item : ctxGroup.items.values())
+ {
+ if (!item.hasModifications())
+ {
+ if (logger.isTraceEnabled())
+ logger.trace("No modifications for {} for {}, {}", item.key(), callback, item);
+ continue;
+ }
+
+ if (futures == null) futures = new ArrayList<>();
+ K key = item.key();
+ Mutation mutation = mutationFunction.apply(item, timestamp);
+ if (logger.isTraceEnabled())
+ logger.trace("Dispatching mutation for {} for {}, {} -> {}", key, callback, item, mutation);
+ Future<?> future = Stage.MUTATION.submit(() -> {
+ try
+ {
+ if (logger.isTraceEnabled())
+ logger.trace("Applying mutation for {} for {}: {}", key, callback, mutation);
+ mutation.apply();
+ if (logger.isTraceEnabled())
+ logger.trace("Completed applying mutation for {} for {}: {}", key, callback, mutation);
+ }
+ catch (Throwable t)
+ {
+ logger.error(String.format("Exception applying mutation for %s for %s: %s", key, callback, mutation), t);
+ throw t;
+ }
+ });
+ cache.addSaveFuture(item.key(), future);
+ futures.add(future);
+ }
+
+ for (AccordState.WriteOnly<K, V> item : ctxGroup.writeOnly.values())
+ {
+ Preconditions.checkState(item.hasModifications());
+ if (futures == null) futures = new ArrayList<>();
+ Mutation mutation = mutationFunction.apply((V) item, timestamp);
+ Future<?> future = Stage.MUTATION.submit((Runnable) mutation::apply);
+ future.addListener(() -> cache.purgeWriteOnly(item.key()), commandStore.executor());
+ item.future(future);
+ futures.add(future);
+ }
+
+ return futures;
+ }
+
+ private Future<?> maybeDispatchWrites(AsyncContext context, Object callback) throws IOException
+ {
+ List<Future<?>> futures = null;
+
+ long timestamp = commandStore.nextSystemTimestampMicros();
+ futures = dispatchWrites(context.commands,
+ commandStore.commandCache(),
+ AccordKeyspace::getCommandMutation,
+ timestamp,
+ commandStore,
+ futures,
+ callback);
+
+ futures = dispatchWrites(context.commandsForKey,
+ commandStore.commandsForKeyCache(),
+ AccordKeyspace::getCommandsForKeyMutation,
+ timestamp,
+ commandStore,
+ futures,
+ callback);
+
+ return futures != null ? FutureCombiner.allOf(futures) : null;
+ }
+
+ private void denormalizeBlockedOn(AccordCommand command,
+ AsyncContext context,
+ Function<AccordCommand, StoredNavigableMap<TxnId, ByteBuffer>> waitingField,
+ Function<AccordCommand, StoredSet.Navigable<TxnId>> blockingField)
+ {
+ StoredNavigableMap<TxnId, ?> waitingOn = waitingField.apply(command);
+ waitingOn.forEachDeletion(deletedId -> {
+ AccordCommand blockedOn = commandForDenormalization(deletedId, context);
+ blockingField.apply(blockedOn).blindRemove(command.txnId());
+ });
+
+ waitingOn.forEachAddition((addedId, unused) -> {
+ AccordCommand blockedOn = commandForDenormalization(addedId, context);
+ blockingField.apply(blockedOn).blindAdd(command.txnId());
+ });
+ }
+
+ private void denormalizeWaitingOnSummaries(AccordCommand command,
+ AsyncContext context,
+ ByteBuffer summary,
+ Function<AccordCommand, StoredNavigableMap<TxnId, ByteBuffer>> waitingField,
+ Function<AccordCommand, StoredSet.Navigable<TxnId>> blockingField)
+ {
+ blockingField.apply(command).getView().forEach(blockingId -> {
+ AccordCommand blocking = commandForDenormalization(blockingId, context);
+ waitingField.apply(blocking).blindPut(command.txnId(), summary.duplicate());
+ });
+ }
+
+ private static <K, V extends AccordState<K>>
+ AccordState<K> getForDenormalization(K key,
+ AccordCommandStore commandStore,
+ AsyncContext.Group<K, V> ctxGroup,
+ AccordStateCache.Instance<K, V> cache,
+ BiFunction<AccordCommandStore, K, AccordState.WriteOnly<K, V>> factory)
+ {
+ V item = ctxGroup.get(key);
+ if (item != null)
+ return item;
+
+ item = cache.getOrNull(key);
+ if (item != null)
+ {
+ ctxGroup.items.put(key, item);
+ return item;
+ }
+
+ return ctxGroup.getOrCreateWriteOnly(key, factory, commandStore);
+ }
+
+ private AccordCommand commandForDenormalization(TxnId txnId, AsyncContext context)
+ {
+ return (AccordCommand) getForDenormalization(txnId, commandStore, context.commands, commandCache, AccordCommand.WriteOnly::new);
+ }
+
+ private AccordCommandsForKey cfkForDenormalization(PartitionKey key, AsyncContext context)
+ {
+ return (AccordCommandsForKey) getForDenormalization(key, commandStore, context.commandsForKey, cfkCache, AccordCommandsForKey.WriteOnly::new);
+ }
+
+ private void denormalize(AccordCommand command, AsyncContext context, Object callback)
+ {
+ if (!command.hasModifications())
+ return;
+
+ // notify commands we're waiting on that they need to update the summaries in our maps
+ if (command.waitingOnCommit.hasModifications())
+ denormalizeBlockedOn(command, context, cmd -> cmd.waitingOnCommit, cmd -> cmd.blockingCommitOn);
+ if (command.waitingOnApply.hasModifications())
+ denormalizeBlockedOn(command, context, cmd -> cmd.waitingOnApply, cmd -> cmd.blockingApplyOn);
+
+ if (command.shouldUpdateDenormalizedWaitingOn())
+ {
+ ByteBuffer summary = AccordPartialCommand.serializer.serialize(command);
+ denormalizeWaitingOnSummaries(command, context, summary, cmd -> cmd.waitingOnCommit, cmd -> cmd.blockingCommitOn);
+ denormalizeWaitingOnSummaries(command, context, summary, cmd -> cmd.waitingOnApply, cmd -> cmd.blockingApplyOn);
+ }
+
+ // There won't be a txn to denormalize against until the command has been preaccepted
+ if (command.status().hasBeen(Status.PreAccepted) && AccordPartialCommand.WithDeps.serializer.needsUpdate(command))
+ {
+ for (Key key : command.txn().keys())
+ {
+ PartitionKey partitionKey = (PartitionKey) key;
+ AccordCommandsForKey cfk = cfkForDenormalization(partitionKey, context);
+ cfk.updateSummaries(command);
+ }
+ }
+
+ if (logger.isTraceEnabled())
+ {
+ context.commands.items.forEach((txnId, cmd) -> logger.trace("Denormalized command {} for {}: {}", txnId, callback, cmd));
+ context.commandsForKey.items.forEach((key, cfk) -> logger.trace("Denormalized cfk {} for {}: {}", key, callback, cfk));
+ }
+ }
+
+ private void denormalizePartial(AccordPartialCommand partial, AsyncContext context, Object callback)
+ {
+ if (!partial.hasRemovedListeners())
+ return;
+
+ logger.trace("Denormalizing partial command change on {} for {}", partial, callback);
+ AccordCommand forUpdate = commandForDenormalization(partial.txnId(), context);
+ partial.forEachRemovedListener(forUpdate::removeListener);
+ }
+
+ private void denormalize(AsyncContext context, Object callback)
+ {
+ // need to clone "values" as denormalize will mutate it
+ new ArrayList<>(context.commands.items.values()).forEach(command -> denormalize(command, context, callback));
+ context.commands.partials.forEach(command -> denormalizePartial(command, context, callback));
+ }
+
+ @VisibleForTesting
+ void setState(State state)
+ {
+ this.state = state;
+ }
+
+ public boolean save(AsyncContext context, BiConsumer<Object, Throwable> callback)
+ {
+ logger.trace("Running save for {} with state {}", callback, state);
+ commandStore.checkInStoreThread();
+ try
+ {
+ switch (state)
+ {
+ case INITIALIZED:
+ setState(State.SETUP);
+ case SETUP:
+ denormalize(context, callback);
+ writeFuture = maybeDispatchWrites(context, callback);
+
+ setState(State.SAVING);
+ case SAVING:
+ if (writeFuture != null && !writeFuture.isSuccess())
+ {
+ logger.trace("Adding callback for write future: {}", callback);
+ writeFuture.addCallback(callback, commandStore.executor());
+ break;
+ }
+ context.commands.items.keySet().forEach(commandStore.commandCache()::cleanupSaveFuture);
+ context.commandsForKey.items.keySet().forEach(commandStore.commandsForKeyCache()::cleanupSaveFuture);
+ setState(State.FINISHED);
+ case FINISHED:
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+
+ logger.trace("Exiting save for {} with state {}", callback, state);
+ return state == State.FINISHED;
+ }
+
+}
diff --git a/src/java/org/apache/cassandra/service/accord/db/AbstractKeyIndexed.java b/src/java/org/apache/cassandra/service/accord/db/AbstractKeyIndexed.java
new file mode 100644
index 0000000000..84bb9cc216
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/db/AbstractKeyIndexed.java
@@ -0,0 +1,188 @@
+/*
+ * 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.db;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.db.marshal.ByteBufferAccessor;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+/**
+ * Contains a map of objects serialized to byte buffers
+ * @param <T>
+ */
+public abstract class AbstractKeyIndexed<T>
+{
+ final NavigableMap<PartitionKey, ByteBuffer> serialized;
+
+ abstract void serialize(T t, DataOutputPlus out, int version) throws IOException;
+ abstract T deserialize(DataInputPlus in, int version) throws IOException;
+ abstract long serializedSize(T t, int version);
+ abstract long emptySizeOnHeap();
+
+ protected ByteBuffer serialize(T item)
+ {
+ int version = MessagingService.current_version;
+ long size = serializedSize(item, version) + TypeSizes.INT_SIZE;
+ try (DataOutputBuffer out = new DataOutputBuffer((int) size))
+ {
+ out.writeInt(version);
+ serialize(item, out, version);
+ return out.buffer(false);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected T deserialize(ByteBuffer bytes)
+ {
+ try (DataInputBuffer in = new DataInputBuffer(bytes, true))
+ {
+ int version = in.readInt();
+ return deserialize(in, version);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AbstractKeyIndexed<?> that = (AbstractKeyIndexed<?>) o;
+ return serialized.equals(that.serialized);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(serialized);
+ }
+
+ @Override
+ public String toString()
+ {
+ return getClass().getSimpleName() + serialized.entrySet().stream()
+ .map(e -> e.getKey() + "=" + deserialize(e.getValue()))
+ .collect(Collectors.joining(", ", "{", "}"));
+ }
+
+ public AbstractKeyIndexed(List<T> items, Function<T, PartitionKey> keyFunction)
+ {
+ this.serialized = new TreeMap<>();
+ for (int i=0, mi=items.size(); i<mi; i++)
+ {
+ T item = items.get(i);
+ PartitionKey key = keyFunction.apply(item);
+ // TODO: support multiple reads/writes per key
+ Preconditions.checkArgument(!this.serialized.containsKey(key));
+ this.serialized.put(key, serialize(item));
+ }
+ }
+
+ public AbstractKeyIndexed(NavigableMap<PartitionKey, ByteBuffer> serialized)
+ {
+ this.serialized = serialized;
+ }
+
+ public T getDeserialized(PartitionKey key)
+ {
+ ByteBuffer bytes = serialized.get(key);
+ if (bytes == null)
+ return null;
+ return deserialize(bytes);
+ }
+
+ public long estimatedSizeOnHeap()
+ {
+ long size = emptySizeOnHeap();
+ for (Map.Entry<PartitionKey, ByteBuffer> entry : serialized.entrySet())
+ {
+ size += entry.getKey().estimatedSizeOnHeap();
+ size += ByteBufferUtil.EMPTY_SIZE_ON_HEAP + ByteBufferAccessor.instance.size(entry.getValue());
+ }
+ return size;
+ }
+
+ static class Serializer<V, S extends AbstractKeyIndexed<V>> implements IVersionedSerializer<S>
+ {
+ private final Function<NavigableMap<PartitionKey, ByteBuffer>, S> factory;
+
+ Serializer(Function<NavigableMap<PartitionKey, ByteBuffer>, S> factory)
+ {
+ this.factory = factory;
+ }
+
+ @Override
+ public void serialize(S items, DataOutputPlus out, int version) throws IOException
+ {
+ out.writeUnsignedVInt(items.serialized.size());
+ for (Map.Entry<PartitionKey, ByteBuffer> entry : items.serialized.entrySet())
+ {
+ PartitionKey.serializer.serialize(entry.getKey(), out, version);
+ ByteBufferUtil.writeWithVIntLength(entry.getValue(), out);
+ }
+ }
+
+ @Override
+ public S deserialize(DataInputPlus in, int version) throws IOException
+ {
+ int size = (int) in.readUnsignedVInt();
+ NavigableMap<PartitionKey, ByteBuffer> items = new TreeMap<>();
+ for (int i=0; i<size; i++)
+ items.put(PartitionKey.serializer.deserialize(in, version), ByteBufferUtil.readWithVIntLength(in));
+ return factory.apply(items);
+ }
+
+ @Override
+ public long serializedSize(S items, int version)
+ {
+ long size = TypeSizes.sizeofUnsignedVInt(items.serialized.size());
+ for (Map.Entry<PartitionKey, ByteBuffer> entry : items.serialized.entrySet())
+ {
+ size += PartitionKey.serializer.serializedSize(entry.getKey());
+ size += ByteBufferUtil.serializedSizeWithVIntLength(entry.getValue());
+ }
+ return size;
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/db/AccordData.java b/src/java/org/apache/cassandra/service/accord/db/AccordData.java
new file mode 100644
index 0000000000..40de220e7b
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/db/AccordData.java
@@ -0,0 +1,189 @@
+/*
+ * 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.db;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+
+import com.google.common.base.Preconditions;
+
+import accord.api.Data;
+import accord.api.Result;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.partitions.FilteredPartition;
+import org.apache.cassandra.db.rows.DeserializationHelper;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.db.rows.UnfilteredRowIteratorSerializer;
+import org.apache.cassandra.db.rows.UnfilteredRowIterators;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.utils.ObjectSizes;
+
+public class AccordData extends AbstractKeyIndexed<FilteredPartition> implements Data, Result, Iterable<FilteredPartition>
+{
+ private static final long EMPTY_SIZE = ObjectSizes.measureDeep(new AccordData());
+
+ private static PartitionKey getKey(FilteredPartition partition)
+ {
+ return new PartitionKey(partition.metadata().id, partition.partitionKey());
+ }
+
+ public AccordData()
+ {
+ this(new TreeMap<>());
+ }
+
+ public AccordData(FilteredPartition partition)
+ {
+ this();
+ put(partition);
+ }
+
+ public AccordData(List<FilteredPartition> items)
+ {
+ super(items, AccordData::getKey);
+ }
+
+ public AccordData(NavigableMap<PartitionKey, ByteBuffer> serialized)
+ {
+ super(serialized);
+ }
+
+ @Override
+ void serialize(FilteredPartition partition, DataOutputPlus out, int version) throws IOException
+ {
+ partitionSerializer.serialize(partition, out, version);
+ }
+
+ @Override
+ FilteredPartition deserialize(DataInputPlus in, int version) throws IOException
+ {
+ return partitionSerializer.deserialize(in, version);
+ }
+
+ @Override
+ long serializedSize(FilteredPartition partition, int version)
+ {
+ return partitionSerializer.serializedSize(partition, version);
+ }
+
+ @Override
+ long emptySizeOnHeap()
+ {
+ return EMPTY_SIZE;
+ }
+
+ private void put(PartitionKey key, ByteBuffer bytes)
+ {
+ // TODO: support multiple partitions (ie: read commands) per partition
+ Preconditions.checkArgument(!serialized.containsKey(key) || serialized.get(key).equals(bytes));
+ serialized.put(key, bytes);
+ }
+
+ void put(PartitionKey key, FilteredPartition partition)
+ {
+ put(key, serialize(partition));
+ }
+
+ void put(FilteredPartition partition)
+ {
+ put(AccordKey.of(partition), partition);
+ }
+
+ FilteredPartition get(PartitionKey key)
+ {
+ return getDeserialized(key);
+ }
+
+ @Override
+ public Iterator<FilteredPartition> iterator()
+ {
+ return serialized.values().stream().map(this::deserialize).iterator();
+ }
+
+ @Override
+ public Data merge(Data data)
+ {
+ AccordData that = (AccordData) data;
+ AccordData merged = new AccordData();
+ //TODO on conflict should we "merge" the partition rather than override?
+ this.serialized.forEach(merged::put);
+ that.serialized.forEach(merged::put);
+ return merged;
+ }
+
+ public static Data merge(Data left, Data right)
+ {
+ if (left == null)
+ return right;
+ if (right == null)
+ return right;
+
+ return left.merge(right);
+ }
+
+ private static final IVersionedSerializer<FilteredPartition> partitionSerializer = new IVersionedSerializer<FilteredPartition>()
+ {
+ @Override
+ public void serialize(FilteredPartition partition, DataOutputPlus out, int version) throws IOException
+ {
+ partition.metadata().id.serialize(out);
+ TableMetadata metadata = Schema.instance.getTableMetadata(partition.metadata().id);
+ try (UnfilteredRowIterator iterator = partition.unfilteredIterator())
+ {
+ UnfilteredRowIteratorSerializer.serializer.serialize(iterator, ColumnFilter.all(metadata), out, version, partition.rowCount());
+ }
+ }
+
+ @Override
+ public FilteredPartition deserialize(DataInputPlus in, int version) throws IOException
+ {
+ TableMetadata metadata = Schema.instance.getTableMetadata(TableId.deserialize(in));
+ Preconditions.checkState(metadata != null);
+ try (UnfilteredRowIterator partition = UnfilteredRowIteratorSerializer.serializer.deserialize(in, version, metadata, ColumnFilter.all(metadata), DeserializationHelper.Flag.FROM_REMOTE))
+ {
+ return new FilteredPartition(UnfilteredRowIterators.filter(partition, 0));
+ }
+ }
+
+ @Override
+ public long serializedSize(FilteredPartition partition, int version)
+ {
+ long size = TableId.serializedSize();
+ TableMetadata metadata = Schema.instance.getTableMetadata(partition.metadata().id);
+ Preconditions.checkState(metadata != null);
+ try (UnfilteredRowIterator iterator = partition.unfilteredIterator())
+ {
+ return size + UnfilteredRowIteratorSerializer.serializer.serializedSize(iterator, ColumnFilter.all(metadata), version, partition.rowCount());
+ }
+ }
+ };
+
+ public static final IVersionedSerializer<AccordData> serializer = new Serializer<>(AccordData::new);
+}
diff --git a/src/java/org/apache/cassandra/service/accord/db/AccordQuery.java b/src/java/org/apache/cassandra/service/accord/db/AccordQuery.java
new file mode 100644
index 0000000000..fe79a353d3
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/db/AccordQuery.java
@@ -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.service.accord.db;
+
+import java.io.IOException;
+
+import javax.annotation.Nullable;
+
+import com.google.common.base.Preconditions;
+
+import accord.api.Data;
+import accord.api.Query;
+import accord.api.Read;
+import accord.api.Result;
+import accord.api.Update;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.utils.ObjectSizes;
+
+public abstract class AccordQuery implements Query
+{
+ public static final AccordQuery ALL = new AccordQuery()
+ {
+ @Override
+ public Result compute(Data data, @Nullable Read read, @Nullable Update update)
+ {
+ return data != null ? (AccordData) data : new AccordData();
+ }
+ };
+
+ public static final AccordQuery NONE = new AccordQuery()
+ {
+ @Override
+ public Result compute(Data data, @Nullable Read read, @Nullable Update update)
+ {
+ return new AccordData();
+ }
+ };
+
+ private static final long SIZE = ObjectSizes.measure(ALL);
+
+ private AccordQuery() {}
+
+ public long estimatedSizeOnHeap()
+ {
+ return SIZE;
+ }
+
+ public static final IVersionedSerializer<AccordQuery> serializer = new IVersionedSerializer<AccordQuery>()
+ {
+ @Override
+ public void serialize(AccordQuery query, DataOutputPlus out, int version) throws IOException
+ {
+ Preconditions.checkArgument(query == ALL || query == NONE);
+ out.writeBoolean(query == ALL);
+ }
+
+ @Override
+ public AccordQuery deserialize(DataInputPlus in, int version) throws IOException
+ {
+ return in.readBoolean() ? ALL : NONE;
+ }
+
+ @Override
+ public long serializedSize(AccordQuery query, int version)
+ {
+ Preconditions.checkArgument(query == ALL || query == NONE);
+ return TypeSizes.sizeof(query == ALL);
+ }
+ };
+}
diff --git a/src/java/org/apache/cassandra/service/accord/db/AccordRead.java b/src/java/org/apache/cassandra/service/accord/db/AccordRead.java
new file mode 100644
index 0000000000..55fe14d665
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/db/AccordRead.java
@@ -0,0 +1,138 @@
+/*
+ * 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.db;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import accord.api.Data;
+import accord.api.DataStore;
+import accord.api.Key;
+import accord.api.Read;
+import accord.local.CommandStore;
+import accord.primitives.Keys;
+import accord.primitives.Timestamp;
+import org.apache.cassandra.concurrent.Stage;
+import org.apache.cassandra.db.ReadExecutionController;
+import org.apache.cassandra.db.SinglePartitionReadCommand;
+import org.apache.cassandra.db.partitions.FilteredPartition;
+import org.apache.cassandra.db.partitions.PartitionIterator;
+import org.apache.cassandra.db.partitions.PartitionIterators;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.service.accord.AccordCommandsForKey;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.concurrent.Future;
+import org.apache.cassandra.utils.concurrent.ImmediateFuture;
+
+public class AccordRead extends AbstractKeyIndexed<SinglePartitionReadCommand> implements Read
+{
+ private static final long EMPTY_SIZE = ObjectSizes.measureDeep(new AccordRead(new TreeMap<>()));
+
+ public AccordRead(List<SinglePartitionReadCommand> items)
+ {
+ super(items, AccordKey::of);
+ }
+
+ public AccordRead(NavigableMap<PartitionKey, ByteBuffer> serialized)
+ {
+ super(serialized);
+ }
+
+ @Override
+ void serialize(SinglePartitionReadCommand command, DataOutputPlus out, int version) throws IOException
+ {
+ SinglePartitionReadCommand.serializer.serialize(command, out, version);
+ }
+
+ @Override
+ SinglePartitionReadCommand deserialize(DataInputPlus in, int version) throws IOException
+ {
+ return (SinglePartitionReadCommand) SinglePartitionReadCommand.serializer.deserialize(in, version);
+ }
+
+ @Override
+ long serializedSize(SinglePartitionReadCommand command, int version)
+ {
+ return SinglePartitionReadCommand.serializer.serializedSize(command, version);
+ }
+
+ @Override
+ long emptySizeOnHeap()
+ {
+ return EMPTY_SIZE;
+ }
+
+ @Override
+ public Keys keys()
+ {
+ return new Keys(serialized.keySet());
+ }
+
+ public String toString()
+ {
+ return "AccordRead{" + super.toString() + '}';
+ }
+
+ @Override
+ public Future<Data> read(Key key, boolean isForWriteTxn, CommandStore commandStore, Timestamp executeAt, DataStore store)
+ {
+ SinglePartitionReadCommand command = getDeserialized((PartitionKey) key);
+ if (command == null)
+ return ImmediateFuture.success(new AccordData());
+
+ AccordCommandsForKey cfk = (AccordCommandsForKey) commandStore.commandsForKey(key);
+ int nowInSeconds = cfk.nowInSecondsFor(executeAt, isForWriteTxn);
+ Future<Data> future = Stage.READ.submit(() -> {
+ SinglePartitionReadCommand read = command.withNowInSec(nowInSeconds);
+ try (ReadExecutionController controller = read.executionController();
+ UnfilteredPartitionIterator partition = read.executeLocally(controller);
+ PartitionIterator iterator = UnfilteredPartitionIterators.filter(partition, read.nowInSec()))
+ {
+ FilteredPartition filtered = FilteredPartition.create(PartitionIterators.getOnlyElement(iterator, read));
+ AccordData result = new AccordData(filtered);
+ return result;
+ }
+ });
+
+ return future;
+ }
+
+ public static AccordRead forCommands(Collection<SinglePartitionReadCommand> commands)
+ {
+ List<SinglePartitionReadCommand> reads = new ArrayList<>(commands);
+ reads.sort(Comparator.comparing(SinglePartitionReadCommand::partitionKey));
+ return new AccordRead(reads);
+ }
+
+ public static final IVersionedSerializer<AccordRead> serializer = new Serializer<>(AccordRead::new);
+}
diff --git a/src/java/org/apache/cassandra/service/accord/db/AccordUpdate.java b/src/java/org/apache/cassandra/service/accord/db/AccordUpdate.java
new file mode 100644
index 0000000000..7abaeb261b
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/db/AccordUpdate.java
@@ -0,0 +1,927 @@
+/*
+ * 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.db;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import accord.api.Data;
+import accord.api.Key;
+import accord.api.Update;
+import accord.api.Write;
+import accord.primitives.Keys;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.Columns;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.RegularAndStaticColumns;
+import org.apache.cassandra.db.SinglePartitionReadCommand;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.db.marshal.ByteBufferAccessor;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.partitions.FilteredPartition;
+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.DeserializationHelper;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.service.accord.serializers.KeySerializers;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.ObjectSizes;
+
+import static org.apache.cassandra.utils.CollectionSerializer.deserializeMap;
+import static org.apache.cassandra.utils.CollectionSerializer.serializeMap;
+import static org.apache.cassandra.utils.CollectionSerializer.serializedSizeMap;
+
+public class AccordUpdate implements Update
+{
+ private static final long EMPTY_SIZE = ObjectSizes.measure(new AccordUpdate(Keys.EMPTY, (ByteBuffer[]) null, (ByteBuffer[]) null));;
+
+ private final Keys keys;
+ private final ByteBuffer[] updates;
+ private final ByteBuffer[] predicates;
+
+ public static abstract class UpdatePredicate
+ {
+ public enum Type
+ {
+ EXISTS("EXISTS"),
+ NOT_EXISTS("NOT EXISTS"),
+ EQUAL("=="),
+ NOT_EQUAL("!="),
+ GREATER_THAN(">"),
+ GREATER_THAN_OR_EQUAL(">="),
+ LESS_THAN("<"),
+ LESS_THAN_OR_EQUAL("<=");
+
+ final String symbol;
+
+ Type(String symbol)
+ {
+ this.symbol = symbol;
+ }
+ }
+
+ final Type type;
+ final TableMetadata table;
+ final DecoratedKey key;
+ final Clustering<?> clustering;
+ final PartitionKey accordKey;
+
+ public UpdatePredicate(Type type, TableMetadata table, DecoratedKey key, Clustering<?> clustering)
+ {
+ this.type = type;
+ this.table = table;
+ this.key = key;
+ this.clustering = clustering;
+ this.accordKey = new PartitionKey(table.id, key);
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ UpdatePredicate predicate = (UpdatePredicate) o;
+ return type == predicate.type && table.equals(predicate.table) && key.equals(predicate.key) && clustering.equals(predicate.clustering);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(type, table, key, clustering);
+ }
+
+ @Override
+ public String toString()
+ {
+ return table.keyspace + '.' + table.name + " ["
+ + key + ", " + clustering.toString(table) + "] " + type.symbol;
+ }
+
+ public PartitionKey partitionKey()
+ {
+ return accordKey;
+ }
+
+ public Type type()
+ {
+ return type;
+ }
+
+ public boolean applies(FilteredPartition partition)
+ {
+ return applies(partition == null ? null : partition.getRow(clustering));
+ }
+
+ public boolean supportedByRead(SinglePartitionReadCommand read)
+ {
+ return read.metadata().id.equals(table.id)
+ && read.partitionKey().equals(key)
+ && read.clusteringIndexFilter().selects(clustering);
+ }
+
+ protected abstract boolean applies(Row row);
+
+ protected abstract void serializeBody(DataOutputPlus out, int version) throws IOException;
+ protected abstract long serializedBodySize(int version);
+ }
+
+ public static class ExistsPredicate extends UpdatePredicate
+ {
+ private static final Set<Type> TYPES = ImmutableSet.of(Type.EXISTS, Type.NOT_EXISTS);
+
+ public ExistsPredicate(Type type, TableMetadata table, DecoratedKey key, Clustering<?> clustering)
+ {
+ super(type, table, key, clustering);
+ Preconditions.checkArgument(TYPES.contains(type));
+ }
+
+ @Override
+ protected boolean applies(Row row)
+ {
+ boolean exists = row != null && !row.isEmpty();
+ switch (type())
+ {
+ case EXISTS:
+ return exists;
+ case NOT_EXISTS:
+ return !exists;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ @Override
+ protected void serializeBody(DataOutputPlus out, int version)
+ {
+
+ }
+
+ @Override
+ protected long serializedBodySize(int version)
+ {
+ return 0;
+ }
+
+ public static ExistsPredicate deserialize(UpdatePredicate.Type type, TableMetadata table, DecoratedKey key, Clustering<?> clustering, DataInputPlus in, int version)
+ {
+ Preconditions.checkArgument(TYPES.contains(type));
+ return new ExistsPredicate(type, table, key, clustering);
+ }
+ }
+
+ public static class ValuePredicate extends UpdatePredicate
+ {
+ private static final Set<Type> TYPES = ImmutableSet.of(Type.EQUAL,
+ Type.NOT_EQUAL,
+ Type.GREATER_THAN,
+ Type.GREATER_THAN_OR_EQUAL,
+ Type.LESS_THAN,
+ Type.LESS_THAN_OR_EQUAL);
+
+ private final ColumnMetadata column;
+ // TODO: add cell path
+ private final ByteBuffer value;
+
+ public ValuePredicate(Type type, TableMetadata table, DecoratedKey key, Clustering<?> clustering,
+ ColumnMetadata column, ByteBuffer value)
+ {
+ super(type, table, key, clustering);
+ Preconditions.checkArgument(TYPES.contains(type));
+ this.column = column;
+ this.value = value;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+ ValuePredicate that = (ValuePredicate) o;
+ return column.equals(that.column) && value.equals(that.value);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(super.hashCode(), column, value);
+ }
+
+ @Override
+ public String toString()
+ {
+ return table.keyspace + '.' + table.name + " ["
+ + key + ", " + clustering.toString(table) + ", " + column.name.toString() + "] "
+ + type.symbol + ' ' + column.type.getString(value);
+ }
+
+ private <T> int compare(Cell<T> cell)
+ {
+ return column.type.compare(cell.value(), cell.accessor(), value, ByteBufferAccessor.instance);
+ }
+
+ @Override
+ public boolean supportedByRead(SinglePartitionReadCommand read)
+ {
+ return super.supportedByRead(read) && read.columnFilter().fetches(column);
+ }
+
+ @Override
+ protected boolean applies(Row row)
+ {
+ if (row == null)
+ return false;
+ Cell<?> cell = row.getCell(column);
+ int cmp = compare(cell);
+ switch (type())
+ {
+ case EQUAL:
+ return cmp == 0;
+ case NOT_EQUAL:
+ return cmp != 0;
+ case GREATER_THAN:
+ return cmp > 0;
+ case GREATER_THAN_OR_EQUAL:
+ return cmp >= 0;
+ case LESS_THAN:
+ return cmp < 0;
+ case LESS_THAN_OR_EQUAL:
+ return cmp <= 0;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ @Override
+ protected void serializeBody(DataOutputPlus out, int version) throws IOException
+ {
+ ByteBufferUtil.writeWithVIntLength(column.name.bytes, out);
+ ByteBufferUtil.writeWithVIntLength(value, out);
+ }
+
+ @Override
+ protected long serializedBodySize(int version)
+ {
+ return ByteBufferUtil.serializedSizeWithVIntLength(column.name.bytes)
+ + ByteBufferUtil.serializedSizeWithVIntLength(value);
+ }
+
+ public static ValuePredicate deserialize(UpdatePredicate.Type type, TableMetadata table, DecoratedKey key, Clustering<?> clustering, DataInputPlus in, int version) throws IOException
+ {
+ Preconditions.checkArgument(TYPES.contains(type));
+ ColumnMetadata column = table.getColumn(ByteBufferUtil.readWithVIntLength(in));
+ ByteBuffer value = ByteBufferUtil.readWithVIntLength(in);
+ return new ValuePredicate(type, table, key, clustering, column, value);
+ }
+ }
+
+ public abstract static class AbstractUpdate
+ {
+ interface Serializer<T extends AbstractUpdate>
+ {
+ void serializeBody(T update, DataOutputPlus out, int version) throws IOException;
+ T deserializeBody(Kind kind, DataInputPlus in, int version) throws IOException;
+ long serializedBodySize(T update, int version);
+ }
+ enum Kind { SIMPLE, APPEND, INCREMENT }
+ public abstract Kind kind();
+ public abstract PartitionKey partitionKey();
+ public abstract PartitionUpdate apply(FilteredPartition partition);
+ public abstract Serializer serializer();
+
+ public static final IVersionedSerializer<AbstractUpdate> serializer = new IVersionedSerializer<AbstractUpdate>()
+ {
+ @Override
+ public void serialize(AbstractUpdate updater, DataOutputPlus out, int version) throws IOException
+ {
+ out.writeInt(updater.kind().ordinal());
+ updater.serializer().serializeBody(updater, out, version);
+ }
+
+ @Override
+ public AbstractUpdate deserialize(DataInputPlus in, int version) throws IOException
+ {
+ Kind kind = Kind.values()[in.readInt()];
+ switch (kind)
+ {
+ case SIMPLE:
+ return SimpleUpdate.serializer.deserializeBody(kind, in, version);
+ case APPEND:
+ return AppendingUpdate.serializer.deserializeBody(kind, in, version);
+ case INCREMENT:
+ return IncrementingUpdate.serializer.deserializeBody(kind, in, version);
+ default: throw new IllegalArgumentException();
+ }
+ }
+
+ @Override
+ public long serializedSize(AbstractUpdate updater, int version)
+ {
+ return TypeSizes.INT_SIZE + updater.serializer().serializedBodySize(updater, version);
+ }
+ };
+ }
+
+ public static class SimpleUpdate extends AbstractUpdate
+ {
+ private final PartitionUpdate update;
+ private final PartitionKey accordKey;
+
+ public SimpleUpdate(PartitionUpdate update)
+ {
+ this.update = update;
+ this.accordKey = new PartitionKey(update.metadata().id, update.partitionKey());
+ }
+
+ @Override
+ public Kind kind()
+ {
+ return Kind.SIMPLE;
+ }
+
+ @Override
+ public PartitionKey partitionKey()
+ {
+ return accordKey;
+ }
+
+ @Override
+ public PartitionUpdate apply(FilteredPartition partition)
+ {
+ return update;
+ }
+
+ @Override
+ public Serializer serializer()
+ {
+ return serializer;
+ }
+
+ static final Serializer<SimpleUpdate> serializer = new Serializer<SimpleUpdate>()
+ {
+ @Override
+ public void serializeBody(SimpleUpdate update, DataOutputPlus out, int version) throws IOException
+ {
+ PartitionUpdate.serializer.serialize(update.update, out, version);
+ }
+
+ @Override
+ public SimpleUpdate deserializeBody(Kind kind, DataInputPlus in, int version) throws IOException
+ {
+ Preconditions.checkArgument(kind == Kind.SIMPLE);
+ return new SimpleUpdate(PartitionUpdate.serializer.deserialize(in, version, DeserializationHelper.Flag.FROM_REMOTE));
+ }
+
+ @Override
+ public long serializedBodySize(SimpleUpdate update, int version)
+ {
+ return PartitionUpdate.serializer.serializedSize(update.update, version);
+ }
+ };
+ }
+
+ private static final IVersionedSerializer<String> strSerializer = new IVersionedSerializer<String>()
+ {
+ @Override
+ public void serialize(String s, DataOutputPlus out, int version) throws IOException { out.writeUTF(s); }
+ @Override
+ public String deserialize(DataInputPlus in, int version) throws IOException { return in.readUTF(); }
+ @Override
+ public long serializedSize(String s, int version) { return TypeSizes.sizeof(s); }
+ };
+
+ private static final IVersionedSerializer<Integer> intSerializer = new IVersionedSerializer<Integer>()
+ {
+ @Override
+ public void serialize(Integer i, DataOutputPlus out, int version) throws IOException { out.writeInt(i); }
+
+ @Override
+ public Integer deserialize(DataInputPlus in, int version) throws IOException { return in.readInt(); }
+
+ @Override
+ public long serializedSize(Integer t, int version) { return TypeSizes.INT_SIZE; }
+ };
+
+ public static class AppendingUpdate extends AbstractUpdate
+ {
+ private final PartitionKey key;
+ private final Map<String, String> appends;
+
+ public AppendingUpdate(PartitionKey key, Map<String, String> appends)
+ {
+ Preconditions.checkArgument(appends != null && !appends.isEmpty());
+ TableMetadata metadata = Schema.instance.getTableMetadata(key.tableId());
+ Preconditions.checkArgument(metadata != null);
+ for (String column : appends.keySet())
+ {
+ ColumnMetadata cdef = metadata.getColumn(new ColumnIdentifier(column, true));
+ Preconditions.checkArgument(cdef != null);
+ Preconditions.checkArgument(cdef.type == UTF8Type.instance);
+ Preconditions.checkArgument(appends.get(column) != null);
+ }
+
+ this.key = key;
+ this.appends = appends;
+ }
+
+ @Override
+ public Kind kind()
+ {
+ return Kind.APPEND;
+ }
+
+ @Override
+ public PartitionKey partitionKey()
+ {
+ return key;
+ }
+
+ private static <T> String getString(Cell<T> cell)
+ {
+ return cell == null || cell.isTombstone() ? "" : UTF8Type.instance.getString(cell.value(), cell.accessor());
+ }
+
+ private static void apply(Row.Builder builder, Row current, ColumnMetadata cdef, String append)
+ {
+ Preconditions.checkArgument(append != null);
+ String value = getString(current.getCell(cdef)) + append;
+ builder.addCell(BufferCell.live(cdef, 0, UTF8Type.instance.fromString(value)));
+ }
+
+ private static Row apply(Row.Builder builder, Row current, Columns columns, Map<ColumnMetadata, String> appendMap)
+ {
+ if (columns.isEmpty())
+ return current;
+
+ builder.newRow(current.clustering());
+ for (ColumnMetadata column : columns)
+ apply(builder, current, column, appendMap.get(column));
+
+ return builder.build();
+ }
+
+ @Override
+ public PartitionUpdate apply(FilteredPartition partition)
+ {
+ Row.Builder rowBuilder = BTreeRow.unsortedBuilder();
+ TableMetadata metadata = Schema.instance.getTableMetadata(key.tableId());
+ RegularAndStaticColumns.Builder cbuilder = RegularAndStaticColumns.builder();
+ Map<ColumnMetadata, String> appendMap = new HashMap<>();
+ for (Map.Entry<String, String> entry : appends.entrySet())
+ {
+ ColumnMetadata cdef = metadata.getColumn(new ColumnIdentifier(entry.getKey(), true));
+ cbuilder.add(cdef);
+ appendMap.put(cdef, entry.getValue());
+ }
+
+ RegularAndStaticColumns columns = cbuilder.build();
+ Row staticRow = apply(rowBuilder, partition.staticRow(), columns.statics, appendMap);
+
+
+ Preconditions.checkArgument(metadata != null);
+ PartitionUpdate.Builder updateBuilder = new PartitionUpdate.Builder(metadata,
+ partition.partitionKey(),
+ columns,
+ staticRow,
+ partition.rowCount());
+ for (Row row : partition)
+ updateBuilder.add(apply(rowBuilder, row, columns.regulars, appendMap));
+
+ return updateBuilder.build();
+ }
+
+ @Override
+ public Serializer serializer()
+ {
+ return serializer;
+ }
+
+ static final Serializer<AppendingUpdate> serializer = new Serializer<AppendingUpdate>()
+ {
+ @Override
+ public void serializeBody(AppendingUpdate update, DataOutputPlus out, int version) throws IOException
+ {
+ PartitionKey.serializer.serialize(update.key, out, version);
+ serializeMap(strSerializer, strSerializer, update.appends, out, version);
+ }
+
+ @Override
+ public AppendingUpdate deserializeBody(Kind kind, DataInputPlus in, int version) throws IOException
+ {
+ Preconditions.checkArgument(kind == Kind.APPEND);
+ return new AppendingUpdate(PartitionKey.serializer.deserialize(in, version),
+ deserializeMap(strSerializer, strSerializer, Maps::newHashMapWithExpectedSize, in, version));
+ }
+
+ @Override
+ public long serializedBodySize(AppendingUpdate update, int version)
+ {
+ return PartitionKey.serializer.serializedSize(update.key) +
+ serializedSizeMap(strSerializer, strSerializer, update.appends, version);
+ }
+ };
+ }
+
+ public static class IncrementingUpdate extends AbstractUpdate
+ {
+ private final PartitionKey key;
+ private final Map<String, Integer> increments;
+
+ public IncrementingUpdate(PartitionKey key, Map<String, Integer> increments)
+ {
+ Preconditions.checkArgument(increments != null && !increments.isEmpty());
+ TableMetadata metadata = Schema.instance.getTableMetadata(key.tableId());
+ Preconditions.checkArgument(metadata != null);
+
+ for (String column : increments.keySet())
+ {
+ ColumnMetadata cdef = metadata.getColumn(new ColumnIdentifier(column, true));
+ Preconditions.checkArgument(cdef != null);
+ Preconditions.checkArgument(cdef.type == Int32Type.instance);
+ Integer val = increments.get(column);
+ Preconditions.checkArgument(val != null && val != 0);
+ }
+ this.key = key;
+ this.increments = increments;
+ }
+
+ @Override
+ public Kind kind()
+ {
+ return Kind.INCREMENT;
+ }
+
+ @Override
+ public PartitionKey partitionKey()
+ {
+ return key;
+ }
+
+ private static <T> int getInt(Cell<T> cell)
+ {
+ return cell == null || cell.isTombstone() ? 0 : Int32Type.instance.compose(cell.value(), cell.accessor());
+ }
+
+ private static void apply(Row.Builder builder, Row current, ColumnMetadata cdef, int increment)
+ {
+ int value = getInt(current.getCell(cdef)) + increment;
+ builder.addCell(BufferCell.live(cdef, 0, Int32Type.instance.decompose(value)));
+ }
+
+ private static Row apply(Row.Builder builder, Row current, Columns columns, Map<ColumnMetadata, Integer> appendMap)
+ {
+ if (columns.isEmpty())
+ return current;
+
+ builder.newRow(current.clustering());
+ for (ColumnMetadata column : columns)
+ apply(builder, current, column, appendMap.get(column));
+
+ return builder.build();
+ }
+
+ @Override
+ public PartitionUpdate apply(FilteredPartition partition)
+ {
+ Row.Builder rowBuilder = BTreeRow.unsortedBuilder();
+ TableMetadata metadata = Schema.instance.getTableMetadata(key.tableId());
+ RegularAndStaticColumns.Builder cbuilder = RegularAndStaticColumns.builder();
+ Map<ColumnMetadata, Integer> incrementMap = new HashMap<>();
+ for (Map.Entry<String, Integer> entry : increments.entrySet())
+ {
+ ColumnMetadata cdef = metadata.getColumn(new ColumnIdentifier(entry.getKey(), true));
+ cbuilder.add(cdef);
+ incrementMap.put(cdef, entry.getValue());
+ }
+
+ RegularAndStaticColumns columns = cbuilder.build();
+ Row staticRow = apply(rowBuilder, partition.staticRow(), columns.statics, incrementMap);
+
+
+ Preconditions.checkArgument(metadata != null);
+ PartitionUpdate.Builder updateBuilder = new PartitionUpdate.Builder(metadata,
+ partition.partitionKey(),
+ columns,
+ staticRow,
+ partition.rowCount());
+ for (Row row : partition)
+ updateBuilder.add(apply(rowBuilder, row, columns.regulars, incrementMap));
+
+ return updateBuilder.build();
+ }
+
+ @Override
+ public Serializer serializer()
+ {
+ return serializer;
+ }
+
+ static final Serializer<IncrementingUpdate> serializer = new Serializer<IncrementingUpdate>()
+ {
+ @Override
+ public void serializeBody(IncrementingUpdate update, DataOutputPlus out, int version) throws IOException
+ {
+ PartitionKey.serializer.serialize(update.key, out, version);
+ serializeMap(strSerializer, intSerializer, update.increments, out, version);
+ }
+
+ @Override
+ public IncrementingUpdate deserializeBody(Kind kind, DataInputPlus in, int version) throws IOException
+ {
+ Preconditions.checkArgument(kind == Kind.INCREMENT);
+ return new IncrementingUpdate(PartitionKey.serializer.deserialize(in, version),
+ deserializeMap(strSerializer, intSerializer, Maps::newHashMapWithExpectedSize, in, version));
+ }
+
+ @Override
+ public long serializedBodySize(IncrementingUpdate update, int version)
+ {
+ return PartitionKey.serializer.serializedSize(update.key) +
+ serializedSizeMap(strSerializer, intSerializer, update.increments, version);
+ }
+ };
+ }
+
+ private static Keys keysFrom(List<AbstractUpdate> updates, List<UpdatePredicate> predicates)
+ {
+ Set<Key> keys = new HashSet<>();
+ for (AbstractUpdate update : updates)
+ keys.add(update.partitionKey());
+ for (UpdatePredicate predicate : predicates)
+ keys.add(new PartitionKey(predicate.table.id, predicate.key));
+
+ return new Keys(keys);
+ }
+
+ public AccordUpdate(List<AbstractUpdate> updates, List<UpdatePredicate> predicates)
+ {
+ this.keys = keysFrom(updates, predicates);
+ this.updates = serialize(updates, AbstractUpdate.serializer);
+ this.predicates = serialize(predicates, predicateSerializer);
+ }
+
+ public AccordUpdate(Keys keys, ByteBuffer[] updates, ByteBuffer[] predicates)
+ {
+ this.keys = keys;
+ this.updates = updates;
+ this.predicates = predicates;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "AccordUpdate{" +
+ "updates=" + Arrays.toString(updates) +
+ ", predicates=" + Arrays.toString(predicates) +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AccordUpdate update = (AccordUpdate) o;
+ return Arrays.equals(updates, update.updates) && Arrays.equals(predicates, update.predicates);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(updates, predicates);
+ }
+
+ @Override
+ public Keys keys()
+ {
+ return keys;
+ }
+
+ @Override
+ public Write apply(Data data)
+ {
+ AccordData read = data != null ? (AccordData) data : new AccordData();
+ for (ByteBuffer bytes : predicates)
+ {
+ UpdatePredicate predicate = deserialize(bytes, predicateSerializer);
+ if (!predicate.applies(read.get(predicate.partitionKey())))
+ return AccordWrite.EMPTY;
+ }
+ NavigableMap<PartitionKey, PartitionUpdate> updateMap = new TreeMap<>();
+ for (AbstractUpdate updater : deserialize(updates, AbstractUpdate.serializer))
+ {
+ PartitionUpdate update = updater.apply(read.get(updater.partitionKey()));
+ PartitionKey key = AccordKey.of(update);
+ if (updateMap.containsKey(key))
+ update = PartitionUpdate.merge(Lists.newArrayList(updateMap.get(key), update));
+ updateMap.put(key, update);
+ }
+ return new AccordWrite(new ArrayList<>(updateMap.values()));
+ }
+
+ UpdatePredicate getPredicate(int i)
+ {
+ return deserialize(predicates[i], predicateSerializer);
+ }
+
+ public long estimatedSizeOnHeap()
+ {
+ long size = EMPTY_SIZE + ObjectSizes.sizeOfReferenceArray(updates.length) + ObjectSizes.sizeOfReferenceArray(predicates.length);
+ for (ByteBuffer buffer : updates)
+ size += ByteBufferUtil.estimatedSizeOnHeap(buffer);
+ for (ByteBuffer buffer : predicates)
+ size += ByteBufferUtil.estimatedSizeOnHeap(buffer);
+ return size;
+ }
+
+ static final IVersionedSerializer<UpdatePredicate> predicateSerializer = new IVersionedSerializer<UpdatePredicate>()
+ {
+ @Override
+ public void serialize(UpdatePredicate predicate, DataOutputPlus out, int version) throws IOException
+ {
+ out.writeInt(predicate.type.ordinal());
+ predicate.table.id.serialize(out);
+ ByteBufferUtil.writeWithVIntLength(predicate.key.getKey(), out);
+
+ Clustering.serializer.serialize(predicate.clustering, out, version, predicate.table.comparator.subtypes());
+
+ predicate.serializeBody(out, version);
+ }
+
+ @Override
+ public UpdatePredicate deserialize(DataInputPlus in, int version) throws IOException
+ {
+ UpdatePredicate.Type type = UpdatePredicate.Type.values()[in.readInt()];
+ TableId tableId = TableId.deserialize(in);
+ TableMetadata table = Schema.instance.getTableMetadata(tableId);
+ DecoratedKey key = table.partitioner.decorateKey(ByteBufferUtil.readWithVIntLength(in));
+ Clustering<?> clustering = Clustering.serializer.deserialize(in, version, table.comparator.subtypes());
+ switch (type)
+ {
+ case EXISTS:
+ case NOT_EXISTS:
+ return ExistsPredicate.deserialize(type, table, key, clustering, in, version);
+ case EQUAL:
+ case NOT_EQUAL:
+ case GREATER_THAN:
+ case GREATER_THAN_OR_EQUAL:
+ case LESS_THAN:
+ case LESS_THAN_OR_EQUAL:
+ return ValuePredicate.deserialize(type, table, key, clustering, in, version);
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ @Override
+ public long serializedSize(UpdatePredicate predicate, int version)
+ {
+ long size = TypeSizes.sizeof(predicate.type.ordinal());
+ size += predicate.table.id.serializedSize();
+ size += ByteBufferUtil.serializedSizeWithVIntLength(predicate.key.getKey());
+ size += Clustering.serializer.serializedSize(predicate.clustering, version, predicate.table.comparator.subtypes());
+ return size + predicate.serializedBodySize(version);
+ }
+ };
+
+ public static final IVersionedSerializer<AccordUpdate> serializer = new IVersionedSerializer<AccordUpdate>()
+ {
+ @Override
+ public void serialize(AccordUpdate update, DataOutputPlus out, int version) throws IOException
+ {
+ KeySerializers.keys.serialize(update.keys, out, version);
+ out.writeInt(update.updates.length);
+ for (ByteBuffer buffer : update.updates)
+ ByteBufferUtil.writeWithVIntLength(buffer, out);
+
+ out.writeInt(update.predicates.length);
+ for (ByteBuffer buffer : update.predicates)
+ ByteBufferUtil.writeWithVIntLength(buffer, out);
+ }
+
+ @Override
+ public AccordUpdate deserialize(DataInputPlus in, int version) throws IOException
+ {
+ Keys keys = KeySerializers.keys.deserialize(in, version);
+ int numUpdate = in.readInt();
+ ByteBuffer[] updates = new ByteBuffer[numUpdate];
+ for (int i=0; i<numUpdate; i++)
+ updates[i] = ByteBufferUtil.readWithVIntLength(in);
+
+ int numPredicate = in.readInt();
+ ByteBuffer[] predicates = new ByteBuffer[numPredicate];
+ for (int i=0; i<numPredicate; i++)
+ predicates[i] = ByteBufferUtil.readWithVIntLength(in);
+
+ return new AccordUpdate(keys, updates, predicates);
+ }
+
+ @Override
+ public long serializedSize(AccordUpdate update, int version)
+ {
+ long size = KeySerializers.keys.serializedSize(update.keys, version);
+
+ size += TypeSizes.sizeof(update.updates.length);
+ for (ByteBuffer buffer : update.updates)
+ size += ByteBufferUtil.serializedSizeWithVIntLength(buffer);
+
+ size += TypeSizes.sizeof(update.predicates.length);
+ for (ByteBuffer buffer : update.predicates)
+ size += ByteBufferUtil.serializedSizeWithVIntLength(buffer);
+
+ return size;
+ }
+ };
+
+ private static <T> ByteBuffer serialize(T item, IVersionedSerializer<T> serializer)
+ {
+ int version = MessagingService.current_version;
+ long size = serializer.serializedSize(item, version) + TypeSizes.INT_SIZE;
+ try (DataOutputBuffer out = new DataOutputBuffer((int) size))
+ {
+ out.writeInt(version);
+ serializer.serialize(item, out, version);
+ return out.buffer(false);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static <T> ByteBuffer[] serialize(List<T> items, IVersionedSerializer<T> serializer)
+ {
+ ByteBuffer[] result = new ByteBuffer[items.size()];
+ for (int i=0,mi=items.size(); i<mi; i++)
+ result[i] = serialize(items.get(i), serializer);
+ return result;
+ }
+
+ private static <T> T deserialize(ByteBuffer bytes, IVersionedSerializer<T> serializer)
+ {
+ try (DataInputBuffer in = new DataInputBuffer(bytes, true))
+ {
+ int version = in.readInt();
+ return serializer.deserialize(in, version);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static <T> List<T> deserialize(ByteBuffer[] buffers, IVersionedSerializer<T> serializer)
+ {
+ List<T> result = new ArrayList<>(buffers.length);
+ for (ByteBuffer bytes : buffers)
+ result.add(deserialize(bytes, serializer));
+ return result;
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/db/AccordWrite.java b/src/java/org/apache/cassandra/service/accord/db/AccordWrite.java
new file mode 100644
index 0000000000..1b8526a4b7
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/db/AccordWrite.java
@@ -0,0 +1,102 @@
+/*
+ * 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.db;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.NavigableMap;
+
+import com.google.common.collect.ImmutableList;
+
+import accord.api.DataStore;
+import accord.api.Key;
+import accord.api.Write;
+import accord.local.CommandStore;
+import accord.primitives.Timestamp;
+import org.apache.cassandra.concurrent.Stage;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.rows.DeserializationHelper;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.service.accord.AccordCommandsForKey;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.concurrent.Future;
+import org.apache.cassandra.utils.concurrent.ImmediateFuture;
+
+public class AccordWrite extends AbstractKeyIndexed<PartitionUpdate> implements Write
+{
+ private static final Future<Void> SUCCESS = ImmediateFuture.success(null);
+ public static final AccordWrite EMPTY = new AccordWrite(ImmutableList.of());
+ private static final long EMPTY_SIZE = ObjectSizes.measureDeep(EMPTY);
+
+ public AccordWrite(List<PartitionUpdate> items)
+ {
+ super(items, AccordKey::of);
+ }
+
+ public AccordWrite(NavigableMap<PartitionKey, ByteBuffer> items)
+ {
+ super(items);
+ }
+
+ @Override
+ void serialize(PartitionUpdate update, DataOutputPlus out, int version) throws IOException
+ {
+ PartitionUpdate.serializer.serialize(update, out, version);
+ }
+
+ @Override
+ PartitionUpdate deserialize(DataInputPlus in, int version) throws IOException
+ {
+ return PartitionUpdate.serializer.deserialize(in, version, DeserializationHelper.Flag.FROM_REMOTE);
+ }
+
+ @Override
+ long serializedSize(PartitionUpdate update, int version)
+ {
+ return PartitionUpdate.serializer.serializedSize(update, version);
+ }
+
+ @Override
+ long emptySizeOnHeap()
+ {
+ return EMPTY_SIZE;
+ }
+
+ @Override
+ public Future<Void> apply(Key key, CommandStore commandStore, Timestamp executeAt, DataStore store)
+ {
+ PartitionUpdate update = getDeserialized((PartitionKey) key);
+ if (update == null)
+ return SUCCESS;
+ AccordCommandsForKey cfk = (AccordCommandsForKey) commandStore.commandsForKey(key);
+ long timestamp = cfk.timestampMicrosFor(executeAt, true);
+ int nowInSeconds = cfk.nowInSecondsFor(executeAt, true);
+ update = new PartitionUpdate.Builder(update, 0).updateAllTimestampAndLocalDeletionTime(timestamp, nowInSeconds).build();
+ Mutation mutation = new Mutation(update);
+ return (Future<Void>) Stage.MUTATION.submit((Runnable) mutation::apply);
+ }
+
+ public static final IVersionedSerializer<AccordWrite> serializer = new Serializer<>(AccordWrite::new);
+}
diff --git a/src/java/org/apache/cassandra/service/accord/serializers/AcceptSerializers.java b/src/java/org/apache/cassandra/service/accord/serializers/AcceptSerializers.java
new file mode 100644
index 0000000000..26a27ed69e
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/serializers/AcceptSerializers.java
@@ -0,0 +1,179 @@
+/*
+ * 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.serializers;
+
+import java.io.IOException;
+
+import accord.messages.Accept;
... 8274 lines suppressed ...
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org