diff --git a/modules/accord b/modules/accord index 08ee5ce1c6..841e139bc8 160000 --- a/modules/accord +++ b/modules/accord @@ -1 +1 @@ -Subproject commit 08ee5ce1c6301201ccaf7d580a6af289ab4c5765 +Subproject commit 841e139bc8a974ac674ce8eae847bd52255ca544 diff --git a/src/java/org/apache/cassandra/config/AccordSpec.java b/src/java/org/apache/cassandra/config/AccordSpec.java index 4861069409..451bfeaa54 100644 --- a/src/java/org/apache/cassandra/config/AccordSpec.java +++ b/src/java/org/apache/cassandra/config/AccordSpec.java @@ -40,6 +40,8 @@ public class AccordSpec // TODO (expected): we should be able to support lower recover delays, at least for txns public volatile DurationSpec.IntMillisecondsBound recover_delay = new DurationSpec.IntMillisecondsBound(5000); public volatile DurationSpec.IntMillisecondsBound range_sync_recover_delay = new DurationSpec.IntMillisecondsBound(10000); + public String slowPreAccept = "30ms <= p50*2 <= 100ms"; + public String slowRead = "30ms <= p50*2 <= 100ms"; public long recoveryDelayFor(TxnId txnId, TimeUnit unit) { diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionIterator.java b/src/java/org/apache/cassandra/db/compaction/CompactionIterator.java index 2086fddc88..3c7b113136 100644 --- a/src/java/org/apache/cassandra/db/compaction/CompactionIterator.java +++ b/src/java/org/apache/cassandra/db/compaction/CompactionIterator.java @@ -85,7 +85,6 @@ import org.apache.cassandra.io.sstable.ISSTableScanner; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataOutputBuffer; -import org.apache.cassandra.journal.KeySupport; import org.apache.cassandra.metrics.TopPartitionTracker; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.CompactionParams.TombstoneOption; @@ -110,7 +109,6 @@ import org.apache.cassandra.service.paxos.uncommitted.PaxosRows; import org.apache.cassandra.utils.TimeUUID; import static accord.local.Cleanup.ERASE; -import static accord.local.Cleanup.TRUNCATE; import static accord.local.Cleanup.TRUNCATE_WITH_OUTCOME; import static accord.local.Cleanup.shouldCleanupPartial; import static com.google.common.base.Preconditions.checkState; @@ -1020,7 +1018,6 @@ public class CompactionIterator extends CompactionInfo.Holder implements Unfilte final Int2ObjectHashMap ranges; final ColumnMetadata recordColumn; final ColumnMetadata versionColumn; - final KeySupport keySupport = JournalKey.SUPPORT; final AccordService service; JournalKey key = null; @@ -1051,7 +1048,7 @@ public class CompactionIterator extends CompactionInfo.Holder implements Unfilte @Override protected void beginPartition(UnfilteredRowIterator partition) { - key = keySupport.deserialize(partition.partitionKey().getKey(), 0, userVersion); + key = AccordKeyspace.JournalColumns.getJournalKey(partition.partitionKey()); serializer = (AccordJournalValueSerializers.FlyweightSerializer) key.type.serializer; builder = serializer.mergerFor(key); maxSeenTimestamp = -1; diff --git a/src/java/org/apache/cassandra/journal/Compactor.java b/src/java/org/apache/cassandra/journal/Compactor.java index 51b2fec97b..4ecfb74091 100644 --- a/src/java/org/apache/cassandra/journal/Compactor.java +++ b/src/java/org/apache/cassandra/journal/Compactor.java @@ -45,7 +45,7 @@ public final class Compactor implements Runnable, Shutdownable synchronized void start() { - if (!journal.params.enableCompaction()) + if (journal.params.enableCompaction()) schedule(journal.params.compactionPeriodMillis(), TimeUnit.MILLISECONDS); } diff --git a/src/java/org/apache/cassandra/journal/InMemoryIndex.java b/src/java/org/apache/cassandra/journal/InMemoryIndex.java index 1f0da7fd28..77fd7352ee 100644 --- a/src/java/org/apache/cassandra/journal/InMemoryIndex.java +++ b/src/java/org/apache/cassandra/journal/InMemoryIndex.java @@ -27,6 +27,7 @@ import javax.annotation.Nullable; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileOutputStreamPlus; +import org.apache.cassandra.journal.StaticSegment.SequentialReader; /** * An index for a segment that's still being updated by journal writers concurrently. @@ -138,17 +139,10 @@ final class InMemoryIndex extends Index { InMemoryIndex index = new InMemoryIndex<>(keySupport, new TreeMap<>(keySupport)); - try (StaticSegment.SequentialReader reader = StaticSegment.sequentialReader(descriptor, keySupport, fsyncedLimit)) + try (SequentialReader reader = StaticSegment.sequentialReader(descriptor, keySupport, fsyncedLimit)) { - int last = -1; while (reader.advance()) - { - int current = reader.offset(); - if (last >= 0) - index.update(reader.key(), last, current); - last = current; - } - + index.update(reader.key(), reader.offset, reader.buffer.position() - reader.offset); } return index; } diff --git a/src/java/org/apache/cassandra/journal/Segment.java b/src/java/org/apache/cassandra/journal/Segment.java index 7f955669cd..77f7c68fea 100644 --- a/src/java/org/apache/cassandra/journal/Segment.java +++ b/src/java/org/apache/cassandra/journal/Segment.java @@ -93,6 +93,7 @@ public abstract class Segment implements Closeable, RefCounted extends Segment ByteBuffer duplicate = buffer.duplicate().position(offset).limit(offset + size); try (DataInputBuffer in = new DataInputBuffer(duplicate, false)) { - return EntrySerializer.tryRead(into, keySupport, duplicate, in, syncedOffsets.syncedOffset(), descriptor.userVersion); + if (!EntrySerializer.tryRead(into, keySupport, duplicate, in, syncedOffsets.syncedOffset(), descriptor.userVersion)) + return false; + + Invariants.checkState(in.available() == 0); + return true; } catch (IOException e) { diff --git a/src/java/org/apache/cassandra/metrics/AccordMetrics.java b/src/java/org/apache/cassandra/metrics/AccordMetrics.java index c9d8e29687..8064dee66a 100644 --- a/src/java/org/apache/cassandra/metrics/AccordMetrics.java +++ b/src/java/org/apache/cassandra/metrics/AccordMetrics.java @@ -31,7 +31,7 @@ import com.codahale.metrics.Counting; import com.codahale.metrics.Histogram; import com.codahale.metrics.Meter; import com.codahale.metrics.Timer; -import org.apache.cassandra.service.accord.AccordService; +import org.apache.cassandra.service.accord.api.AccordTimeService; import static java.util.concurrent.TimeUnit.MICROSECONDS; import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; @@ -206,7 +206,7 @@ public class AccordMetrics @Override public void onStable(Command cmd) { - long now = AccordService.now(); + long now = AccordTimeService.nowMicros(); AccordMetrics metrics = forTransaction(cmd.txnId()); if (metrics != null) { @@ -218,7 +218,7 @@ public class AccordMetrics @Override public void onExecuted(Command cmd) { - long now = AccordService.now(); + long now = AccordTimeService.nowMicros(); AccordMetrics metrics = forTransaction(cmd.txnId()); if (metrics != null) { @@ -232,7 +232,7 @@ public class AccordMetrics @Override public void onApplied(Command cmd, long applyStartTimestamp) { - long now = AccordService.now(); + long now = AccordTimeService.nowMicros(); AccordMetrics metrics = forTransaction(cmd.txnId()); if (metrics != null) { @@ -270,7 +270,7 @@ public class AccordMetrics AccordMetrics metrics = forTransaction(txnId); if (metrics != null) { - long now = AccordService.now(); + long now = AccordTimeService.nowMicros(); metrics.recoveryDuration.update(now - recoveryTimestamp.hlc(), MICROSECONDS); metrics.recoveryDelay.update(recoveryTimestamp.hlc() - txnId.hlc(), MICROSECONDS); diff --git a/src/java/org/apache/cassandra/net/Verb.java b/src/java/org/apache/cassandra/net/Verb.java index f146a5381a..bb1c3b61ee 100644 --- a/src/java/org/apache/cassandra/net/Verb.java +++ b/src/java/org/apache/cassandra/net/Verb.java @@ -309,56 +309,57 @@ public enum Verb DATA_MOVEMENT_EXECUTED_REQ (817, P1, rpcTimeout, MISC, () -> DataMovement.Status.serializer, () -> DataMovements.instance, DATA_MOVEMENT_EXECUTED_RSP ), // accord - ACCORD_SIMPLE_RSP (119, P2, writeTimeout, IMMEDIATE, () -> EnumSerializer.simpleReply, RESPONSE_HANDLER ), - ACCORD_PRE_ACCEPT_RSP (120, P2, writeTimeout, IMMEDIATE, () -> PreacceptSerializers.reply, RESPONSE_HANDLER ), - ACCORD_PRE_ACCEPT_REQ (121, P2, writeTimeout, IMMEDIATE, () -> PreacceptSerializers.request, AccordService::verbHandlerOrNoop, ACCORD_PRE_ACCEPT_RSP ), - ACCORD_ACCEPT_RSP (122, P2, writeTimeout, IMMEDIATE, () -> AcceptSerializers.reply, RESPONSE_HANDLER ), - ACCORD_ACCEPT_REQ (123, P2, writeTimeout, IMMEDIATE, () -> AcceptSerializers.request, AccordService::verbHandlerOrNoop, ACCORD_ACCEPT_RSP ), - ACCORD_ACCEPT_INVALIDATE_REQ (124, P2, writeTimeout, IMMEDIATE, () -> AcceptSerializers.invalidate, AccordService::verbHandlerOrNoop, ACCORD_ACCEPT_RSP ), - ACCORD_READ_RSP (125, P2, writeTimeout, IMMEDIATE, () -> ReadDataSerializers.reply, RESPONSE_HANDLER ), - ACCORD_READ_REQ (126, P2, writeTimeout, IMMEDIATE, () -> ReadDataSerializers.readData, AccordService::verbHandlerOrNoop, ACCORD_READ_RSP ), - ACCORD_COMMIT_REQ (127, P2, writeTimeout, IMMEDIATE, () -> CommitSerializers.request, AccordService::verbHandlerOrNoop, ACCORD_READ_RSP ), - ACCORD_COMMIT_INVALIDATE_REQ (128, P2, writeTimeout, IMMEDIATE, () -> CommitSerializers.invalidate, AccordService::verbHandlerOrNoop ), - ACCORD_APPLY_RSP (129, P2, writeTimeout, IMMEDIATE, () -> ApplySerializers.reply, RESPONSE_HANDLER ), - ACCORD_APPLY_REQ (130, P2, writeTimeout, IMMEDIATE, () -> ApplySerializers.request, AccordService::verbHandlerOrNoop, ACCORD_APPLY_RSP ), - ACCORD_BEGIN_RECOVER_RSP (131, P2, writeTimeout, IMMEDIATE, () -> RecoverySerializers.reply, RESPONSE_HANDLER ), - ACCORD_BEGIN_RECOVER_REQ (132, P2, writeTimeout, IMMEDIATE, () -> RecoverySerializers.request, AccordService::verbHandlerOrNoop, ACCORD_BEGIN_RECOVER_RSP ), - ACCORD_BEGIN_INVALIDATE_RSP (133, P2, writeTimeout, IMMEDIATE, () -> BeginInvalidationSerializers.reply, RESPONSE_HANDLER ), - ACCORD_BEGIN_INVALIDATE_REQ (134, P2, writeTimeout, IMMEDIATE, () -> BeginInvalidationSerializers.request, AccordService::verbHandlerOrNoop, ACCORD_BEGIN_INVALIDATE_RSP ), - ACCORD_AWAIT_RSP (136, P2, writeTimeout, IMMEDIATE, () -> AwaitSerializer.syncReply, RESPONSE_HANDLER ), - ACCORD_AWAIT_REQ (135, P2, writeTimeout, IMMEDIATE, () -> AwaitSerializer.request, AccordService::verbHandlerOrNoop, ACCORD_AWAIT_RSP ), - ACCORD_AWAIT_ASYNC_RSP_REQ (137, P2, writeTimeout, IMMEDIATE, () -> AwaitSerializer.asyncReply, AccordService::verbHandlerOrNoop ), - ACCORD_WAIT_UNTIL_APPLIED_REQ (138, P2, writeTimeout, IMMEDIATE, () -> ReadDataSerializers.waitUntilApplied, AccordService::verbHandlerOrNoop, ACCORD_READ_RSP ), - ACCORD_INFORM_DURABLE_REQ (140, P2, writeTimeout, IMMEDIATE, () -> InformDurableSerializers.request, AccordService::verbHandlerOrNoop, ACCORD_SIMPLE_RSP ), - ACCORD_CHECK_STATUS_RSP (141, P2, writeTimeout, IMMEDIATE, () -> CheckStatusSerializers.reply, RESPONSE_HANDLER ), - ACCORD_CHECK_STATUS_REQ (142, P2, writeTimeout, IMMEDIATE, () -> CheckStatusSerializers.request, AccordService::verbHandlerOrNoop, ACCORD_CHECK_STATUS_RSP ), - ACCORD_CALCULATE_DEPS_RSP (143, P2, writeTimeout, IMMEDIATE, () -> CalculateDepsSerializers.reply, RESPONSE_HANDLER ), - ACCORD_CALCULATE_DEPS_REQ (144, P2, longTimeout, IMMEDIATE, () -> CalculateDepsSerializers.request, AccordService::verbHandlerOrNoop, ACCORD_CALCULATE_DEPS_RSP), - ACCORD_GET_EPHMRL_READ_DEPS_RSP (161, P2, writeTimeout, IMMEDIATE, () -> GetEphmrlReadDepsSerializers.reply, RESPONSE_HANDLER ), - ACCORD_GET_EPHMRL_READ_DEPS_REQ (162, P2, writeTimeout, IMMEDIATE, () -> GetEphmrlReadDepsSerializers.request, AccordService::verbHandlerOrNoop, ACCORD_GET_EPHMRL_READ_DEPS_RSP), - ACCORD_GET_MAX_CONFLICT_RSP (163, P2, writeTimeout, IMMEDIATE, () -> GetMaxConflictSerializers.reply, RESPONSE_HANDLER ), - ACCORD_GET_MAX_CONFLICT_REQ (164, P2, writeTimeout, IMMEDIATE, () -> GetMaxConflictSerializers.request, AccordService::verbHandlerOrNoop, ACCORD_GET_MAX_CONFLICT_RSP), - ACCORD_FETCH_DATA_RSP (145, P2, writeTimeout, IMMEDIATE, () -> FetchSerializers.reply, RESPONSE_HANDLER ), - ACCORD_FETCH_DATA_REQ (146, P2, writeTimeout, IMMEDIATE, () -> FetchSerializers.request, AccordService::verbHandlerOrNoop, ACCORD_FETCH_DATA_RSP ), - ACCORD_SET_SHARD_DURABLE_REQ (147, P2, writeTimeout, MISC, () -> SetDurableSerializers.shardDurable, AccordService::verbHandlerOrNoop, ACCORD_SIMPLE_RSP ), - ACCORD_SET_GLOBALLY_DURABLE_REQ (148, P2, writeTimeout, MISC, () -> SetDurableSerializers.globallyDurable,AccordService::verbHandlerOrNoop, ACCORD_SIMPLE_RSP ), - ACCORD_QUERY_DURABLE_BEFORE_RSP (149, P2, writeTimeout, IMMEDIATE, () -> QueryDurableBeforeSerializers.reply, RESPONSE_HANDLER ), - ACCORD_QUERY_DURABLE_BEFORE_REQ (150, P2, writeTimeout, IMMEDIATE, () -> QueryDurableBeforeSerializers.request,AccordService::verbHandlerOrNoop, ACCORD_QUERY_DURABLE_BEFORE_RSP ), + ACCORD_SIMPLE_RSP (119, P2, writeTimeout, IMMEDIATE, () -> EnumSerializer.simpleReply, AccordService::responseHandlerOrNoop ), + ACCORD_PRE_ACCEPT_RSP (120, P2, writeTimeout, IMMEDIATE, () -> PreacceptSerializers.reply, AccordService::responseHandlerOrNoop ), + ACCORD_PRE_ACCEPT_REQ (121, P2, writeTimeout, IMMEDIATE, () -> PreacceptSerializers.request, AccordService::requestHandlerOrNoop, ACCORD_PRE_ACCEPT_RSP ), + ACCORD_ACCEPT_RSP (122, P2, writeTimeout, IMMEDIATE, () -> AcceptSerializers.reply, AccordService::responseHandlerOrNoop ), + ACCORD_ACCEPT_REQ (123, P2, writeTimeout, IMMEDIATE, () -> AcceptSerializers.request, AccordService::requestHandlerOrNoop, ACCORD_ACCEPT_RSP ), + ACCORD_ACCEPT_INVALIDATE_REQ (124, P2, writeTimeout, IMMEDIATE, () -> AcceptSerializers.invalidate, AccordService::requestHandlerOrNoop, ACCORD_ACCEPT_RSP ), + ACCORD_READ_RSP (125, P2, readTimeout, IMMEDIATE, () -> ReadDataSerializers.reply, AccordService::responseHandlerOrNoop ), + ACCORD_READ_REQ (126, P2, readTimeout, IMMEDIATE, () -> ReadDataSerializers.readData, AccordService::requestHandlerOrNoop, ACCORD_READ_RSP ), + ACCORD_COMMIT_REQ (127, P2, writeTimeout, IMMEDIATE, () -> CommitSerializers.request, AccordService::requestHandlerOrNoop, ACCORD_READ_RSP ), + ACCORD_COMMIT_INVALIDATE_REQ (128, P2, writeTimeout, IMMEDIATE, () -> CommitSerializers.invalidate, AccordService::requestHandlerOrNoop ), + ACCORD_APPLY_RSP (129, P2, writeTimeout, IMMEDIATE, () -> ApplySerializers.reply, AccordService::responseHandlerOrNoop ), + ACCORD_APPLY_REQ (130, P2, writeTimeout, IMMEDIATE, () -> ApplySerializers.request, AccordService::requestHandlerOrNoop, ACCORD_APPLY_RSP ), + ACCORD_BEGIN_RECOVER_RSP (131, P2, writeTimeout, IMMEDIATE, () -> RecoverySerializers.reply, AccordService::responseHandlerOrNoop ), + ACCORD_BEGIN_RECOVER_REQ (132, P2, writeTimeout, IMMEDIATE, () -> RecoverySerializers.request, AccordService::requestHandlerOrNoop, ACCORD_BEGIN_RECOVER_RSP ), + ACCORD_BEGIN_INVALIDATE_RSP (133, P2, writeTimeout, IMMEDIATE, () -> BeginInvalidationSerializers.reply, AccordService::responseHandlerOrNoop ), + ACCORD_BEGIN_INVALIDATE_REQ (134, P2, writeTimeout, IMMEDIATE, () -> BeginInvalidationSerializers.request, AccordService::requestHandlerOrNoop, ACCORD_BEGIN_INVALIDATE_RSP ), + ACCORD_AWAIT_RSP (136, P2, writeTimeout, IMMEDIATE, () -> AwaitSerializer.syncReply, AccordService::responseHandlerOrNoop ), + ACCORD_AWAIT_REQ (135, P2, writeTimeout, IMMEDIATE, () -> AwaitSerializer.request, AccordService::requestHandlerOrNoop, ACCORD_AWAIT_RSP ), + ACCORD_AWAIT_ASYNC_RSP_REQ (137, P2, writeTimeout, IMMEDIATE, () -> AwaitSerializer.asyncReply, AccordService::requestHandlerOrNoop ), + ACCORD_WAIT_UNTIL_APPLIED_REQ (138, P2, writeTimeout, IMMEDIATE, () -> ReadDataSerializers.waitUntilApplied, AccordService::requestHandlerOrNoop, ACCORD_READ_RSP ), + ACCORD_INFORM_DURABLE_REQ (140, P2, writeTimeout, IMMEDIATE, () -> InformDurableSerializers.request, AccordService::requestHandlerOrNoop, ACCORD_SIMPLE_RSP ), + ACCORD_CHECK_STATUS_RSP (141, P2, writeTimeout, IMMEDIATE, () -> CheckStatusSerializers.reply, AccordService::responseHandlerOrNoop ), + ACCORD_CHECK_STATUS_REQ (142, P2, writeTimeout, IMMEDIATE, () -> CheckStatusSerializers.request, AccordService::requestHandlerOrNoop, ACCORD_CHECK_STATUS_RSP ), + ACCORD_CALCULATE_DEPS_RSP (143, P2, writeTimeout, IMMEDIATE, () -> CalculateDepsSerializers.reply, AccordService::responseHandlerOrNoop ), + ACCORD_CALCULATE_DEPS_REQ (144, P2, longTimeout, IMMEDIATE, () -> CalculateDepsSerializers.request, AccordService::requestHandlerOrNoop, ACCORD_CALCULATE_DEPS_RSP), + ACCORD_GET_EPHMRL_READ_DEPS_RSP (161, P2, writeTimeout, IMMEDIATE, () -> GetEphmrlReadDepsSerializers.reply, AccordService::responseHandlerOrNoop ), + ACCORD_GET_EPHMRL_READ_DEPS_REQ (162, P2, writeTimeout, IMMEDIATE, () -> GetEphmrlReadDepsSerializers.request, AccordService::requestHandlerOrNoop, ACCORD_GET_EPHMRL_READ_DEPS_RSP), + ACCORD_GET_MAX_CONFLICT_RSP (163, P2, writeTimeout, IMMEDIATE, () -> GetMaxConflictSerializers.reply, AccordService::responseHandlerOrNoop ), + ACCORD_GET_MAX_CONFLICT_REQ (164, P2, writeTimeout, IMMEDIATE, () -> GetMaxConflictSerializers.request, AccordService::requestHandlerOrNoop, ACCORD_GET_MAX_CONFLICT_RSP), + ACCORD_FETCH_DATA_RSP (145, P2, writeTimeout, IMMEDIATE, () -> FetchSerializers.reply, AccordService::responseHandlerOrNoop ), + ACCORD_FETCH_DATA_REQ (146, P2, writeTimeout, IMMEDIATE, () -> FetchSerializers.request, AccordService::requestHandlerOrNoop, ACCORD_FETCH_DATA_RSP ), + ACCORD_SET_SHARD_DURABLE_REQ (147, P2, writeTimeout, MISC, () -> SetDurableSerializers.shardDurable, AccordService::requestHandlerOrNoop, ACCORD_SIMPLE_RSP ), + ACCORD_SET_GLOBALLY_DURABLE_REQ (148, P2, writeTimeout, MISC, () -> SetDurableSerializers.globallyDurable,AccordService::requestHandlerOrNoop, ACCORD_SIMPLE_RSP ), + ACCORD_QUERY_DURABLE_BEFORE_RSP (149, P2, writeTimeout, IMMEDIATE, () -> QueryDurableBeforeSerializers.reply, AccordService::responseHandlerOrNoop ), + ACCORD_QUERY_DURABLE_BEFORE_REQ (150, P2, writeTimeout, IMMEDIATE, () -> QueryDurableBeforeSerializers.request,AccordService::requestHandlerOrNoop, ACCORD_QUERY_DURABLE_BEFORE_RSP ), - ACCORD_SYNC_NOTIFY_REQ (151, P2, writeTimeout, IMMEDIATE, () -> Notification.listSerializer, () -> AccordSyncPropagator.verbHandler, ACCORD_SIMPLE_RSP ), + ACCORD_SYNC_NOTIFY_RSP (151, P2, writeTimeout, IMMEDIATE, () -> EnumSerializer.simpleReply, RESPONSE_HANDLER), + ACCORD_SYNC_NOTIFY_REQ (152, P2, writeTimeout, IMMEDIATE, () -> Notification.listSerializer, () -> AccordSyncPropagator.verbHandler, ACCORD_SYNC_NOTIFY_RSP ), - ACCORD_APPLY_AND_WAIT_REQ (152, P2, writeTimeout, IMMEDIATE, () -> ReadDataSerializers.readData, AccordService::verbHandlerOrNoop, ACCORD_READ_RSP), + ACCORD_APPLY_AND_WAIT_REQ (153, P2, writeTimeout, IMMEDIATE, () -> ReadDataSerializers.readData, AccordService::requestHandlerOrNoop, ACCORD_READ_RSP), - CONSENSUS_KEY_MIGRATION (153, P1, writeTimeout, MUTATION, () -> ConsensusKeyMigrationFinished.serializer,() -> ConsensusKeyMigrationState.consensusKeyMigrationFinishedHandler), + CONSENSUS_KEY_MIGRATION (154, P1, writeTimeout, MUTATION, () -> ConsensusKeyMigrationFinished.serializer,() -> ConsensusKeyMigrationState.consensusKeyMigrationFinishedHandler), - ACCORD_INTEROP_READ_RSP (154, P2, writeTimeout, IMMEDIATE, () -> AccordInteropRead.replySerializer, RESPONSE_HANDLER), - ACCORD_INTEROP_READ_REQ (155, P2, writeTimeout, IMMEDIATE, () -> AccordInteropRead.requestSerializer, AccordService::verbHandlerOrNoop, ACCORD_INTEROP_READ_RSP), - ACCORD_INTEROP_COMMIT_REQ (156, P2, writeTimeout, IMMEDIATE, () -> AccordInteropCommit.serializer, AccordService::verbHandlerOrNoop, ACCORD_INTEROP_READ_RSP), - ACCORD_INTEROP_READ_REPAIR_RSP (157, P2, writeTimeout, IMMEDIATE, () -> AccordInteropReadRepair.replySerializer, RESPONSE_HANDLER), - ACCORD_INTEROP_READ_REPAIR_REQ (158, P2, writeTimeout, IMMEDIATE, () -> AccordInteropReadRepair.requestSerializer, AccordService::verbHandlerOrNoop, ACCORD_INTEROP_READ_REPAIR_RSP), - ACCORD_INTEROP_APPLY_REQ (160, P2, writeTimeout, IMMEDIATE, () -> AccordInteropApply.serializer, AccordService::verbHandlerOrNoop, ACCORD_APPLY_RSP), - ACCORD_FETCH_MIN_EPOCH_RSP (166, P2, writeTimeout, IMMEDIATE, () -> FetchMinEpoch.Response.serializer, RESPONSE_HANDLER), - ACCORD_FETCH_MIN_EPOCH_REQ (165, P2, writeTimeout, IMMEDIATE, () -> FetchMinEpoch.serializer, () -> FetchMinEpoch.handler, ACCORD_FETCH_MIN_EPOCH_RSP), + ACCORD_INTEROP_READ_RSP (155, P2, writeTimeout, IMMEDIATE, () -> AccordInteropRead.replySerializer, AccordService::responseHandlerOrNoop), + ACCORD_INTEROP_READ_REQ (156, P2, writeTimeout, IMMEDIATE, () -> AccordInteropRead.requestSerializer, AccordService::requestHandlerOrNoop, ACCORD_INTEROP_READ_RSP), + ACCORD_INTEROP_COMMIT_REQ (157, P2, writeTimeout, IMMEDIATE, () -> AccordInteropCommit.serializer, AccordService::requestHandlerOrNoop, ACCORD_INTEROP_READ_RSP), + ACCORD_INTEROP_READ_REPAIR_RSP (158, P2, writeTimeout, IMMEDIATE, () -> AccordInteropReadRepair.replySerializer, AccordService::responseHandlerOrNoop), + ACCORD_INTEROP_READ_REPAIR_REQ (159, P2, writeTimeout, IMMEDIATE, () -> AccordInteropReadRepair.requestSerializer, AccordService::requestHandlerOrNoop, ACCORD_INTEROP_READ_REPAIR_RSP), + ACCORD_INTEROP_APPLY_REQ (160, P2, writeTimeout, IMMEDIATE, () -> AccordInteropApply.serializer, AccordService::requestHandlerOrNoop, ACCORD_APPLY_RSP), + ACCORD_FETCH_MIN_EPOCH_RSP (166, P2, writeTimeout, IMMEDIATE, () -> FetchMinEpoch.Response.serializer, RESPONSE_HANDLER), + ACCORD_FETCH_MIN_EPOCH_REQ (165, P2, writeTimeout, IMMEDIATE, () -> FetchMinEpoch.serializer, () -> FetchMinEpoch.handler, ACCORD_FETCH_MIN_EPOCH_RSP), // generic failure response FAILURE_RSP (99, P0, noTimeout, REQUEST_RESPONSE, () -> RequestFailure.serializer, RESPONSE_HANDLER ), @@ -490,6 +491,11 @@ public enum Verb return expiration.applyAsLong(NANOSECONDS); } + public long expiresAfter(TimeUnit units) + { + return expiration.applyAsLong(units); + } + // this is a little hacky, but reduces the number of parameters up top public boolean isResponse() { diff --git a/src/java/org/apache/cassandra/service/RetryStrategy.java b/src/java/org/apache/cassandra/service/RetryStrategy.java new file mode 100644 index 0000000000..7f48612a8f --- /dev/null +++ b/src/java/org/apache/cassandra/service/RetryStrategy.java @@ -0,0 +1,299 @@ +/* + * 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; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.service.TimeoutStrategy.LatencySourceFactory; +import org.apache.cassandra.service.TimeoutStrategy.Wait; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.DoubleSupplier; +import java.util.function.LongBinaryOperator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.lang.Math.*; +import static java.util.Arrays.stream; +import static java.util.concurrent.TimeUnit.*; +import static org.apache.cassandra.service.TimeoutStrategy.parseWait; +import static org.apache.cassandra.utils.Clock.Global.nanoTime; + +/** + *

A strategy for making retry timing decisions for operations. + * The strategy is defined by four factors:

    + *
  • {@link #min} + *
  • {@link #max} + *
  • {@link #spread} + *
  • {@link #waitRandomizer} + *
+ * + *

The first three represent time periods, and may be defined dynamically based on a simple calculation over:

    + *
  • {@code pX()} recent experienced latency distribution for successful operations, + * e.g. {@code p50(rw)} the maximum of read and write median latencies, + * {@code p999(r)} the 99.9th percentile of read latencies + *
  • {@code attempts} the number of failed attempts made by the operation so far + *
  • {@code constant} a user provided floating point constant + *
+ * + *

Their calculation may take any of these forms + *

  • constant {@code $constant$[mu]s} + *
  • dynamic constant {@code pX() * constant} + *
  • dynamic linear {@code pX() * constant * attempts} + *
  • dynamic exponential {@code pX() * constant ^ attempts} + * + *

    Furthermore, the dynamic calculations can be bounded with a min/max, like so: + * {@code min[mu]s <= dynamic expr <= max[mu]s} + * + * e.g. + *

  • {@code 10ms <= p50(rw)*0.66} + *
  • {@code 10ms <= p95(rw)*1.8^attempts <= 100ms} + *
  • {@code 5ms <= p50(rw)*0.5} + * + *

    These calculations are put together to construct a range from which we draw a random number. + * The period we wait for {@code X} will be drawn so that {@code min <= X < max}. + * + *

    With the constraint that {@code max} must be {@code spread} greater than {@code min}, + * but no greater than its expression-defined maximum. {@code max} will be increased up until + * this point, after which {@code min} will be decreased until this gap is imposed. + * + *

    The {@link #waitRandomizer} property specifies the manner in which a random value is drawn from the range. + * It is defined using one of the following specifiers: + *

  • uniform + *
  • exp($power$) or exponential($power$) + *
  • qexp($power$) or qexponential($power$) or quantizedexponential($power$) + * + * The uniform specifier is self-explanatory, selecting all values in the range with equal probability. + * The exponential specifier draws values towards the end of the range with higher probability, raising + * a floating point number in the range [0..1.0) to the power provided, and translating the resulting value + * to a uniform value in the range. + * The quantized exponential specifier partitions the range into {@code attempts} buckets, then applies the pure + * exponential approach to draw values from [0..attempts), before drawing a uniform value from the corresponding bucket + */ +public class RetryStrategy +{ + private static final Pattern RANDOMIZER = Pattern.compile( + "uniform|exp(onential)?[(](?[0-9.]+)[)]|q(uantized)?exp(onential)?[(](?[0-9.]+)[)]"); + + final static WaitRandomizerFactory randomizers = new WaitRandomizerFactory(){}; + + protected interface WaitRandomizer + { + long wait(long min, long max, int attempts); + } + + interface WaitRandomizerFactory + { + default LongBinaryOperator uniformLongSupplier() { return (min, max) -> ThreadLocalRandom.current().nextLong(min, max); } // DO NOT USE METHOD HANDLES (want to fetch afresh each time) + default DoubleSupplier uniformDoubleSupplier() { return () -> ThreadLocalRandom.current().nextDouble(); } + + default WaitRandomizer uniform() { return new Uniform(uniformLongSupplier()); } + default WaitRandomizer exponential(double power) { return new Exponential(uniformLongSupplier(), uniformDoubleSupplier(), power); } + default WaitRandomizer quantizedExponential(double power) { return new QuantizedExponential(uniformLongSupplier(), uniformDoubleSupplier(), power); } + + static class Uniform implements WaitRandomizer + { + final LongBinaryOperator uniformLong; + + public Uniform(LongBinaryOperator uniformLong) + { + this.uniformLong = uniformLong; + } + + @Override + public long wait(long min, long max, int attempts) + { + return uniformLong.applyAsLong(min, max); + } + } + + static abstract class AbstractExponential implements WaitRandomizer + { + final LongBinaryOperator uniformLong; + final DoubleSupplier uniformDouble; + final double power; + + public AbstractExponential(LongBinaryOperator uniformLong, DoubleSupplier uniformDouble, double power) + { + this.uniformLong = uniformLong; + this.uniformDouble = uniformDouble; + this.power = power; + } + } + + static class Exponential extends AbstractExponential + { + public Exponential(LongBinaryOperator uniformLong, DoubleSupplier uniformDouble, double power) + { + super(uniformLong, uniformDouble, power); + } + + @Override + public long wait(long min, long max, int attempts) + { + if (attempts == 1) + return uniformLong.applyAsLong(min, max); + + double p = uniformDouble.getAsDouble(); + long delta = max - min; + delta *= Math.pow(p, power); + return max - delta; + } + } + + static class QuantizedExponential extends AbstractExponential + { + public QuantizedExponential(LongBinaryOperator uniformLong, DoubleSupplier uniformDouble, double power) + { + super(uniformLong, uniformDouble, power); + } + + @Override + public long wait(long min, long max, int attempts) + { + long quanta = (max - min) / attempts; + if (attempts == 1 || quanta == 0) + return uniformLong.applyAsLong(min, max); + + double p = uniformDouble.getAsDouble(); + int base = (int) (attempts * Math.pow(p, power)); + return max - ThreadLocalRandom.current().nextLong(quanta * base, quanta * (base + 1)); + } + } + } + + public final WaitRandomizer waitRandomizer; + public final Wait min, max, spread; + + public RetryStrategy(String waitRandomizer, String min, String max, String spread, LatencySourceFactory latencies) + { + this.waitRandomizer = parseWaitRandomizer(waitRandomizer); + this.min = parseBound(min, true, latencies); + this.max = parseBound(max, false, latencies); + this.spread = parseBound(spread, true, latencies); + } + + protected RetryStrategy(WaitRandomizer waitRandomizer, Wait min, Wait max, Wait spread) + { + this.waitRandomizer = waitRandomizer; + this.min = min; + this.max = max; + this.spread = spread; + } + + protected Wait parseBound(String spec, boolean isMin, LatencySourceFactory latencies) + { + long defaultMaxMicros = DatabaseDescriptor.getRpcTimeout(MICROSECONDS); + return parseWait(spec, 0, defaultMaxMicros, isMin ? 0 : defaultMaxMicros, latencies); + } + + protected long computeWaitUntil(int attempts) + { + long wait = computeWait(attempts); + return nanoTime() + MICROSECONDS.toNanos(wait); + } + + protected long computeWait(int attempts) + { + long minWaitMicros = min.get(attempts); + long maxWaitMicros = max.get(attempts); + long spreadMicros = spread.get(attempts); + + if (minWaitMicros + spreadMicros > maxWaitMicros) + { + maxWaitMicros = minWaitMicros + spreadMicros; + if (maxWaitMicros > this.max.max) + { + maxWaitMicros = this.max.max; + minWaitMicros = max(this.min.min, min(this.min.max, maxWaitMicros - spreadMicros)); + } + } + + return waitRandomizer.wait(minWaitMicros, maxWaitMicros, attempts); + } + + public static class ParsedStrategy + { + public final String waitRandomizer, min, max, spread; + public final RetryStrategy strategy; + + protected ParsedStrategy(String waitRandomizer, String min, String max, String spread, RetryStrategy strategy) + { + this.waitRandomizer = waitRandomizer; + this.min = min; + this.max = max; + this.spread = spread; + this.strategy = strategy; + } + + public String toString() + { + return "min=" + min + ",max=" + max + ",spread=" + spread + ",random=" + waitRandomizer; + } + } + + @VisibleForTesting + public static ParsedStrategy parseStrategy(String spec, LatencySourceFactory latencies, ParsedStrategy defaultStrategy) + { + String[] args = spec.split(","); + String waitRandomizer = find(args, "random"); + String min = find(args, "min"); + String max = find(args, "max"); + String spread = find(args, "spread"); + if (spread == null) + spread = find(args, "delta"); + + if (waitRandomizer == null) waitRandomizer = defaultStrategy.waitRandomizer; + if (min == null) min = defaultStrategy.min; + if (max == null) max = defaultStrategy.max; + if (spread == null) spread = defaultStrategy.spread; + + RetryStrategy strategy = new RetryStrategy(waitRandomizer, min, max, spread, latencies); + return new ParsedStrategy(waitRandomizer, min, max, spread, strategy); + } + + protected static String find(String[] args, String param) + { + return stream(args).filter(s -> s.startsWith(param + '=')) + .map(s -> s.substring(param.length() + 1)) + .findFirst().orElse(null); + } + + static WaitRandomizer parseWaitRandomizer(String input) + { + return parseWaitRandomizer(input, randomizers); + } + + static WaitRandomizer parseWaitRandomizer(String input, WaitRandomizerFactory randomizers) + { + Matcher m = RANDOMIZER.matcher(input); + if (!m.matches()) + throw new IllegalArgumentException(input + " does not match" + RANDOMIZER); + + String exp; + exp = m.group("exp"); + if (exp != null) + return randomizers.exponential(Double.parseDouble(exp)); + exp = m.group("qexp"); + if (exp != null) + return randomizers.quantizedExponential(Double.parseDouble(exp)); + return randomizers.uniform(); + } +} diff --git a/src/java/org/apache/cassandra/service/TimeoutStrategy.java b/src/java/org/apache/cassandra/service/TimeoutStrategy.java new file mode 100644 index 0000000000..8cbc67698f --- /dev/null +++ b/src/java/org/apache/cassandra/service/TimeoutStrategy.java @@ -0,0 +1,368 @@ +/* + * 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; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.codahale.metrics.Snapshot; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.metrics.ClientRequestMetrics; +import org.apache.cassandra.utils.NoSpamLogger; + +import static java.lang.Double.parseDouble; +import static java.lang.Integer.parseInt; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.Math.pow; +import static java.util.concurrent.TimeUnit.MICROSECONDS; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.apache.cassandra.utils.Clock.Global.nanoTime; + +/** + *

    A strategy for making timeout decisions for operations. This is a simplified single-value version of + * the RetryStrategy + * + *

    This represent a computed time period, that may be defined dynamically based on a simple calculation over:

      + *
    • {@code pX()} recent experienced latency distribution for successful operations, + * e.g. {@code p50(rw)} the maximum of read and write median latencies, + * {@code p999(r)} the 99.9th percentile of read latencies + *
    • {@code attempts} the number of failed attempts made by the operation so far + *
    • {@code constant} a user provided floating point constant + *
    + * + *

    The calculation may take any of these forms + *

  • constant {@code $constant$[mu]s} + *
  • dynamic constant {@code pX() * constant} + *
  • dynamic linear {@code pX() * constant * attempts} + *
  • dynamic exponential {@code pX() * constant ^ attempts} + * + *

    Furthermore, the dynamic calculations can be bounded with a min/max, like so: + * {@code min[mu]s <= dynamic expr <= max[mu]s} + * + * e.g. + *

  • {@code 10ms <= p50(rw)*0.66} + *
  • {@code 10ms <= p95(rw)*1.8^attempts <= 100ms} + *
  • {@code 5ms <= p50(rw)*0.5} + * + * TODO (expected): permit simple constant addition (e.g. p50+5ms) + * TODO (required): track separate stats per-DC as inputs to these decisions + */ +public class TimeoutStrategy +{ + private static final Logger logger = LoggerFactory.getLogger(TimeoutStrategy.class); + + static final Pattern BOUND = Pattern.compile( + "(?0|[0-9]+[mu]s)" + + "|((?0|[0-9]+[mu]s) *<= *)?" + + "(p(?[0-9]+)(\\((?r|w|rw|wr)\\))?|(?0|[0-9]+[mu]s))" + + "\\s*([*]\\s*(?[0-9.]+)?\\s*(?[*^]\\s*attempts)?)?" + + "( *<= *(?0|[0-9]+[mu]s))?"); + static final Pattern TIME = Pattern.compile( + "0|([0-9]+)ms|([0-9]+)us"); + + // Factories can be useful for testing purposes, to supply custom implementations of selectors and modifiers. + final static LatencySupplierFactory selectors = new LatencySupplierFactory(){}; + final static LatencyModifierFactory modifiers = new LatencyModifierFactory(){}; + + interface LatencyModifierFactory + { + default LatencyModifier identity() { return (l, a) -> l; } + default LatencyModifier multiply(double constant) { return (l, a) -> saturatedCast(l * constant); } + default LatencyModifier multiplyByAttempts(double multiply) { return (l, a) -> saturatedCast(l * multiply * a); } + default LatencyModifier multiplyByAttemptsExp(double base) { return (l, a) -> saturatedCast(l * pow(base, a)); } + } + + interface LatencySupplier + { + long get(); + } + + public interface LatencySource + { + long get(double percentile); + } + + interface LatencySupplierFactory + { + default LatencySupplier constant(long latency) { return () -> latency; } + default LatencySupplier percentile(double percentile, LatencySource latencies) { return () -> latencies.get(percentile); } + } + + public interface LatencySourceFactory + { + LatencySource source(String params); + + static LatencySourceFactory rw(ClientRequestMetrics reads, ClientRequestMetrics writes) + { + return new ReadWriteLatencySourceFactory(reads, writes); + } + + static LatencySourceFactory of(ClientRequestMetrics latencies) + { + LatencySource source = new TimeLimitedLatencySupplier(latencies.latency::getSnapshot, 10, SECONDS); + return ignore -> source; + } + } + + public static class ReadWriteLatencySourceFactory implements LatencySourceFactory + { + final LatencySource reads, writes; + + public ReadWriteLatencySourceFactory(ClientRequestMetrics reads, ClientRequestMetrics writes) + { + this(reads.latency::getSnapshot, writes.latency::getSnapshot); + } + + public ReadWriteLatencySourceFactory(Supplier reads, Supplier writes) + { + this.reads = new TimeLimitedLatencySupplier(reads, 10, SECONDS); + this.writes = new TimeLimitedLatencySupplier(writes, 10, SECONDS); + } + + @Override + public LatencySource source(String rw) + { + if (rw.length() == 2) + return percentile -> Math.max(reads.get(percentile), writes.get(percentile)); + else if ("r".equals(rw)) + return reads; + else + return writes; + } + } + + interface LatencyModifier + { + long modify(long latency, int attempts); + } + + static class SnapshotAndTime + { + final long validUntil; + final Snapshot snapshot; + + SnapshotAndTime(long validUntil, Snapshot snapshot) + { + this.validUntil = validUntil; + this.snapshot = snapshot; + } + } + + static class TimeLimitedLatencySupplier extends AtomicReference implements LatencySource + { + final Supplier snapshotSupplier; + final long validForNanos; + + TimeLimitedLatencySupplier(Supplier snapshotSupplier, long time, TimeUnit units) + { + this.snapshotSupplier = snapshotSupplier; + this.validForNanos = units.toNanos(time); + } + + private Snapshot getSnapshot() + { + long now = nanoTime(); + + SnapshotAndTime cur = get(); + if (cur != null && cur.validUntil > now) + return cur.snapshot; + + Snapshot newSnapshot = snapshotSupplier.get(); + SnapshotAndTime next = new SnapshotAndTime(now + validForNanos, newSnapshot); + if (compareAndSet(cur, next)) + return next.snapshot; + + return accumulateAndGet(next, (a, b) -> a.validUntil > b.validUntil ? a : b).snapshot; + } + + @Override + public long get(double percentile) + { + return (long)getSnapshot().getValue(percentile); + } + } + + public static class Wait + { + final long min, max, onFailure; + final LatencyModifier modifier; + final LatencySupplier supplier; + + Wait(long min, long max, long onFailure, LatencyModifier modifier, LatencySupplier supplier) + { + Preconditions.checkArgument(min<=max, "min (%s) must be less than or equal to max (%s)", min, max); + this.min = min; + this.max = max; + this.onFailure = onFailure; + this.modifier = modifier; + this.supplier = supplier; + } + + long get(int attempts) + { + try + { + long base = supplier.get(); + return max(min, min(max, modifier.modify(base, attempts))); + } + catch (Throwable t) + { + NoSpamLogger.getLogger(logger, 1L, MINUTES).info("", t); + return onFailure; + } + } + + public String toString() + { + return "Bound{" + + "min=" + min + + ", max=" + max + + ", onFailure=" + onFailure + + ", modifier=" + modifier + + ", supplier=" + supplier + + '}'; + } + } + + final Wait wait; + + public TimeoutStrategy(String spec, LatencySourceFactory latencies) + { + this.wait = parseWait(spec, latencies); + } + + public long computeWait(int attempts, TimeUnit units) + { + return units.convert(wait.get(attempts), MICROSECONDS); + } + + public long computeWaitUntil(int attempts) + { + long nanos = computeWait(attempts, NANOSECONDS); + return nanoTime() + nanos; + } + + protected Wait parseWait(String spec, LatencySourceFactory latencies) + { + long defaultMicros = DatabaseDescriptor.getRpcTimeout(MICROSECONDS); + return parseWait(spec, 0, defaultMicros, defaultMicros, latencies); + } + + private static LatencySupplier parseLatencySupplier(Matcher m, LatencySupplierFactory selectors, LatencySourceFactory latenciesFactory) + { + String perc = m.group("perc"); + if (perc == null) + return selectors.constant(parseInMicros(m.group("constbase"))); + + LatencySource latencies = latenciesFactory.source(m.group("rw")); + double percentile = parseDouble("0." + perc); + return selectors.percentile(percentile, latencies); + } + + private static LatencyModifier parseLatencyModifier(Matcher m, LatencyModifierFactory modifiers) + { + String mod = m.group("mod"); + if (mod == null) + return modifiers.identity(); + + double modifier = parseDouble(mod); + + String modkind = m.group("modkind"); + if (modkind == null) + return modifiers.multiply(modifier); + + if (modkind.startsWith("*")) + return modifiers.multiplyByAttempts(modifier); + else if (modkind.startsWith("^")) + return modifiers.multiplyByAttemptsExp(modifier); + else + throw new IllegalArgumentException("Unrecognised attempt modifier: " + modkind); + } + + static long saturatedCast(double v) + { + if (v > Long.MAX_VALUE) + return Long.MAX_VALUE; + return (long) v; + } + + public static Wait parseWait(String input, long defaultMin, long defaultMax, long onFailure, LatencySourceFactory latencies) + { + return parseWait(input, defaultMin, defaultMax, onFailure, latencies, selectors, modifiers); + } + + @VisibleForTesting + public static Wait parseWait(String input, long defaultMinMicros, long defaultMaxMicros, long onFailure, LatencySourceFactory latencies, LatencySupplierFactory selectors, LatencyModifierFactory modifiers) + { + Matcher m = BOUND.matcher(input); + if (!m.matches()) + throw new IllegalArgumentException(input + " does not match " + BOUND); + + String maybeConst = m.group("const"); + if (maybeConst != null) + { + long v = parseInMicros(maybeConst); + return new Wait(v, v, v, modifiers.identity(), selectors.constant(v)); + } + + long min = parseInMicros(m.group("min"), defaultMinMicros); + long max = parseInMicros(m.group("max"), defaultMaxMicros); + return new Wait(min, max, onFailure, parseLatencyModifier(m, modifiers), parseLatencySupplier(m, selectors, latencies)); + } + + private static long parseInMicros(String input, long orElse) + { + if (input == null) + return orElse; + + return parseInMicros(input); + } + + private static long parseInMicros(String input) + { + Matcher m = TIME.matcher(input); + if (!m.matches()) + throw new IllegalArgumentException(input + " does not match " + TIME); + + String text; + if (null != (text = m.group(1))) + return parseInt(text) * 1000; + else if (null != (text = m.group(2))) + return parseInt(text); + else + return 0; + } + + private static String orElse(Supplier get, String orElse) + { + String result = get.get(); + return result != null ? result : orElse; + } +} diff --git a/src/java/org/apache/cassandra/service/accord/AccordCommandStore.java b/src/java/org/apache/cassandra/service/accord/AccordCommandStore.java index 455154b4ad..bba5a70826 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordCommandStore.java +++ b/src/java/org/apache/cassandra/service/accord/AccordCommandStore.java @@ -80,11 +80,8 @@ import org.apache.cassandra.utils.concurrent.AsyncPromise; import org.apache.cassandra.utils.concurrent.Promise; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; -import static accord.primitives.SaveStatus.Applying; import static accord.primitives.Status.Committed; import static accord.primitives.Status.Invalidated; -import static accord.primitives.Status.PreApplied; -import static accord.primitives.Status.Stable; import static accord.primitives.Status.Truncated; import static accord.utils.Invariants.checkState; @@ -310,7 +307,6 @@ public class AccordCommandStore extends CommandStore journal.persistStoreState(id, fieldUpdates, onFlush); } - @Nullable @VisibleForTesting public void appendToLog(Command before, Command after, Runnable onFlush) @@ -626,10 +622,7 @@ public class AccordCommandStore extends CommandStore safeStore -> { SafeCommand safeCommand = safeStore.unsafeGet(txnId); Command local = safeCommand.current(); - if (local.is(Stable) || local.is(PreApplied)) - Commands.maybeExecute(safeStore, safeCommand, local, true, true); - else if (local.saveStatus().compareTo(Applying) >= 0 && !local.hasBeen(Truncated)) - Commands.applyWrites(safeStore, context, local).begin(agent); + Commands.maybeExecute(safeStore, safeCommand, local, true, true); }) .begin((unused, throwable) -> { if (throwable != null) diff --git a/src/java/org/apache/cassandra/service/accord/AccordJournal.java b/src/java/org/apache/cassandra/service/accord/AccordJournal.java index 38bd9f9101..26b868e5d1 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordJournal.java +++ b/src/java/org/apache/cassandra/service/accord/AccordJournal.java @@ -20,7 +20,6 @@ package org.apache.cassandra.service.accord; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.NavigableMap; import java.util.Set; @@ -32,6 +31,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import accord.impl.ErasedSafeCommand; +import accord.impl.TimestampsForKey; import accord.local.Cleanup; import accord.local.Command; import accord.local.CommandStores; @@ -113,7 +113,7 @@ public class AccordJournal implements IJournal, Shutdownable throw new UnsupportedOperationException(); } }, - new AccordSegmentCompactor<>(JournalKey.SUPPORT, params.userVersion())); + new AccordSegmentCompactor<>(params.userVersion())); this.journalTable = new AccordJournalTable<>(journal, JournalKey.SUPPORT, params.userVersion()); this.params = params; } @@ -151,7 +151,7 @@ public class AccordJournal implements IJournal, Shutdownable @Override public void shutdown() { - Invariants.checkState(status == Status.REPLAY || status == Status.STARTED); + Invariants.checkState(status == Status.REPLAY || status == Status.STARTED, "%s", status); status = Status.TERMINATING; journal.shutdown(); status = Status.TERMINATED; @@ -357,23 +357,10 @@ public class AccordJournal implements IJournal, Shutdownable public void replay() { logger.info("Starting journal replay."); + TimestampsForKey.unsafeSetReplay(true); CommandsForKey.disableLinearizabilityViolationsReporting(); AccordKeyspace.truncateAllCaches(); - // TODO (expected): optimize replay memory footprint - class ToApply - { - final JournalKey key; - final Command command; - - ToApply(JournalKey key, Command command) - { - this.key = key; - this.command = command; - } - } - - List toApply = new ArrayList<>(); try (AccordJournalTable.KeyOrderIterator iter = journalTable.readAll()) { JournalKey key; @@ -406,23 +393,17 @@ public class AccordJournal implements IJournal, Shutdownable { Command command = builder.construct(); AccordCommandStore commandStore = (AccordCommandStore) node.commandStores().forId(key.commandStoreId); - commandStore.loader().load(command).get(); + AccordCommandStore.Loader loader = commandStore.loader(); + loader.load(command).get(); if (command.saveStatus().compareTo(SaveStatus.Stable) >= 0 && !command.hasBeen(Truncated)) - toApply.add(new ToApply(key, command)); + loader.apply(command); } } - toApply.sort(Comparator.comparing(v -> v.command.executeAt())); - for (ToApply apply : toApply) - { - AccordCommandStore commandStore = (AccordCommandStore) node.commandStores().forId(apply.key.commandStoreId); - logger.info("Apply {}", apply.command); - commandStore.loader().apply(apply.command); - } - logger.info("Waiting for command stores to quiesce."); ((AccordCommandStores)node.commandStores()).waitForQuiescense(); CommandsForKey.enableLinearizabilityViolationsReporting(); + TimestampsForKey.unsafeSetReplay(false); logger.info("Finished journal replay."); status = Status.STARTED; } @@ -488,4 +469,9 @@ public class AccordJournal implements IJournal, Shutdownable } } } + + public void unsafeSetStarted() + { + status = Status.STARTED; + } } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/service/accord/AccordJournalTable.java b/src/java/org/apache/cassandra/service/accord/AccordJournalTable.java index 5935a910b5..5212b575ea 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordJournalTable.java +++ b/src/java/org/apache/cassandra/service/accord/AccordJournalTable.java @@ -49,7 +49,6 @@ import org.apache.cassandra.io.sstable.ISSTableScanner; import org.apache.cassandra.io.sstable.format.SSTableReader; 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.journal.EntrySerializer.EntryHolder; import org.apache.cassandra.journal.Journal; import org.apache.cassandra.journal.KeySupport; @@ -58,7 +57,7 @@ import org.apache.cassandra.schema.ColumnMetadata; import static org.apache.cassandra.io.sstable.SSTableReadsListener.NOOP_LISTENER; -public class AccordJournalTable +public class AccordJournalTable { private static final IntHashSet SENTINEL_HOSTS = new IntHashSet(); @@ -170,8 +169,7 @@ public class AccordJournalTable private void readAllFromTable(K key, TableRecordConsumer onEntry) { - DecoratedKey pk = makePartitionKey(cfs, key, keySupport, accordJournalVersion); - + DecoratedKey pk = AccordKeyspace.JournalColumns.decorate(key); try (RefViewFragment view = cfs.selectAndReference(View.select(SSTableSet.LIVE, pk))) { if (view.sstables.isEmpty()) @@ -209,20 +207,6 @@ public class AccordJournalTable onEntry.accept(descriptor, position, into.key, into.value, into.hosts, into.userVersion); } - public static DecoratedKey makePartitionKey(ColumnFamilyStore cfs, K key, KeySupport keySupport, int version) - { - try (DataOutputBuffer out = new DataOutputBuffer(keySupport.serializedSize(version))) - { - keySupport.serialize(key, out, version); - return cfs.decorateKey(out.buffer(false)); - } - catch (IOException e) - { - // can only throw if (key) serializer is buggy - throw new RuntimeException("Could not serialize key " + key + ", this shouldn't be possible", e); - } - } - @SuppressWarnings("resource") // Auto-closeable iterator will release related resources public KeyOrderIterator readAll() { @@ -249,7 +233,7 @@ public class AccordJournalTable : UnfilteredPartitionIterators.merge(scanners, UnfilteredPartitionIterators.MergeListener.NOOP); } - public K key() + public JournalKey key() { if (partition == null) { @@ -259,7 +243,7 @@ public class AccordJournalTable return null; } - return keySupport.deserialize(partition.partitionKey().getKey(), 0, accordJournalVersion); + return AccordKeyspace.JournalColumns.getJournalKey(partition.partitionKey()); } protected void readAllForKey(K key, RecordConsumer recordConsumer) @@ -318,7 +302,8 @@ public class AccordJournalTable @Override public K key() { - K tableKey = tableIterator.key(); + // TODO (expected): fix generics mismatch here + K tableKey = (K)tableIterator.key(); K journalKey = staticSegmentIterator.key(); if (tableKey == null) return journalKey; @@ -331,7 +316,7 @@ public class AccordJournalTable @Override public void readAllForKey(K key, RecordConsumer reader) { - K tableKey = tableIterator.key(); + K tableKey = (K)tableIterator.key(); K journalKey = staticSegmentIterator.key(); if (journalKey != null && keySupport.compare(journalKey, key) == 0) staticSegmentIterator.readAllForKey(key, (segment, position, key1, buffer, hosts, userVersion) -> { diff --git a/src/java/org/apache/cassandra/service/accord/AccordKeyspace.java b/src/java/org/apache/cassandra/service/accord/AccordKeyspace.java index 4a17c43794..7a4760b7f6 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordKeyspace.java +++ b/src/java/org/apache/cassandra/service/accord/AccordKeyspace.java @@ -83,6 +83,7 @@ import org.apache.cassandra.db.filter.DataLimits; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.db.marshal.ByteArrayAccessor; import org.apache.cassandra.db.marshal.ByteBufferAccessor; +import org.apache.cassandra.db.marshal.ByteType; import org.apache.cassandra.db.marshal.BytesType; import org.apache.cassandra.db.marshal.CompositeType; import org.apache.cassandra.db.marshal.Int32Type; @@ -230,12 +231,14 @@ public class AccordKeyspace parse(JOURNAL, "accord journal", "CREATE TABLE %s (" - + "key blob," + + "store_id int," + + "type tinyint," + + "id blob," + "descriptor bigint," + "offset int," + "user_version int," + "record blob," - + "PRIMARY KEY(key, descriptor, offset)" + + "PRIMARY KEY((store_id, type, id), descriptor, offset)" + ") WITH CLUSTERING ORDER BY (descriptor DESC, offset DESC) WITH compression = {'class':'NoopCompressor'};") .partitioner(new LocalPartitioner(BytesType.instance)) .build(); @@ -1350,6 +1353,68 @@ public class AccordKeyspace } } + public static class JournalColumns + { + static final ClusteringComparator keyComparator = Journal.partitionKeyAsClusteringComparator(); + static final CompositeType partitionKeyType = (CompositeType) Journal.partitionKeyType; + public static final ColumnMetadata store_id = getColumn(Journal, "store_id"); + public static final ColumnMetadata type = getColumn(Journal, "type"); + public static final ColumnMetadata id = getColumn(Journal, "id"); + public static final ColumnMetadata record = getColumn(Journal, "record"); + + public static DecoratedKey decorate(JournalKey key) + { + ByteBuffer id = ByteBuffer.allocate(CommandSerializers.txnId.serializedSize()); + CommandSerializers.txnId.serialize(key.id, id); + id.flip(); + ByteBuffer pk = keyComparator.make(key.commandStoreId, (byte)key.type.id, id).serializeAsPartitionKey(); + Invariants.checkState(getTxnId(splitPartitionKey(pk)).equals(key.id)); + return Journal.partitioner.decorateKey(pk); + } + + public static ByteBuffer[] splitPartitionKey(DecoratedKey key) + { + return JournalColumns.partitionKeyType.split(key.getKey()); + } + + public static ByteBuffer[] splitPartitionKey(ByteBuffer key) + { + return JournalColumns.partitionKeyType.split(key); + } + + public static int getStoreId(DecoratedKey pk) + { + return getStoreId(splitPartitionKey(pk)); + } + + public static int getStoreId(ByteBuffer[] partitionKeyComponents) + { + return Int32Type.instance.compose(partitionKeyComponents[store_id.position()]); + } + + public static JournalKey.Type getType(ByteBuffer[] partitionKeyComponents) + { + return JournalKey.Type.fromId(ByteType.instance.compose(partitionKeyComponents[type.position()])); + } + + public static TxnId getTxnId(DecoratedKey key) + { + return getTxnId(splitPartitionKey(key)); + } + + public static TxnId getTxnId(ByteBuffer[] partitionKeyComponents) + { + ByteBuffer buffer = partitionKeyComponents[id.position()]; + return CommandSerializers.txnId.deserialize(buffer, buffer.position()); + } + + public static JournalKey getJournalKey(DecoratedKey key) + { + ByteBuffer[] parts = splitPartitionKey(key); + return new JournalKey(getTxnId(parts), getType(parts), getStoreId(parts)); + } + } + private static EpochDiskState saveEpochDiskState(EpochDiskState diskState) { String cql = "INSERT INTO " + ACCORD_KEYSPACE_NAME + '.' + EPOCH_METADATA + ' ' + diff --git a/src/java/org/apache/cassandra/service/accord/AccordMessageSink.java b/src/java/org/apache/cassandra/service/accord/AccordMessageSink.java index eda0f75b87..968068481d 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordMessageSink.java +++ b/src/java/org/apache/cassandra/service/accord/AccordMessageSink.java @@ -26,12 +26,18 @@ import java.util.List; import java.util.Map; import java.util.Set; +import accord.impl.RequestCallbacks; import accord.messages.*; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; + +import org.apache.cassandra.config.AccordSpec; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.metrics.ClientRequestsMetricsHolder; +import org.apache.cassandra.service.TimeoutStrategy; +import org.apache.cassandra.service.TimeoutStrategy.LatencySourceFactory; import org.apache.cassandra.utils.Clock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,6 +56,7 @@ import org.apache.cassandra.net.ResponseContext; import org.apache.cassandra.net.Verb; import static accord.messages.MessageType.Kind.REMOTE; +import static java.util.concurrent.TimeUnit.NANOSECONDS; public class AccordMessageSink implements MessageSink { @@ -201,17 +208,28 @@ public class AccordMessageSink implements MessageSink private final Agent agent; private final MessageDelivery messaging; private final AccordEndpointMapper endpointMapper; + private final RequestCallbacks callbacks; + // TODO (required): make hot property + private TimeoutStrategy slowPreaccept, slowRead; - public AccordMessageSink(Agent agent, MessageDelivery messaging, AccordEndpointMapper endpointMapper) + public AccordMessageSink(Agent agent, MessageDelivery messaging, AccordEndpointMapper endpointMapper, RequestCallbacks callbacks) { + AccordSpec config = DatabaseDescriptor.getAccord(); + if (config != null) + { + // TODO (expected): introduce better metrics, esp. for preaccept, but also to disambiguate DC latencies + slowPreaccept = new TimeoutStrategy(config.slowPreAccept, LatencySourceFactory.of(ClientRequestsMetricsHolder.accordReadMetrics)); + slowRead = new TimeoutStrategy(config.slowRead, LatencySourceFactory.of(ClientRequestsMetricsHolder.accordReadMetrics)); + } this.agent = agent; this.messaging = messaging; this.endpointMapper = endpointMapper; + this.callbacks = callbacks; } - public AccordMessageSink(Agent agent, AccordConfigurationService endpointMapper) + public AccordMessageSink(Agent agent, AccordConfigurationService endpointMapper, RequestCallbacks callbacks) { - this(agent, MessagingService.instance(), endpointMapper); + this(agent, MessagingService.instance(), endpointMapper, callbacks); } @Override @@ -237,24 +255,42 @@ public class AccordMessageSink implements MessageSink return txnRequest.txnId.domain().isRange(); } + // TODO (expected): permit bulk send to save esp. on callback registration (and combine records) @Override public void send(Node.Id to, Request request, AgentExecutor executor, Callback callback) { Verb verb = getVerb(request); Preconditions.checkNotNull(verb, "Verb is null for type %s", request.type()); - Message message; - if (isRangeBarrier(request)) + long nowNanos = Clock.Global.nanoTime(); + long expiresAtNanos; + if (isRangeBarrier(request)) expiresAtNanos = nowNanos + DatabaseDescriptor.getAccordRangeBarrierTimeoutNanos(); + else expiresAtNanos = nowNanos + verb.expiresAfterNanos(); + long delayedAtNanos = Long.MAX_VALUE; + switch (verb) { - long nowNanos = Clock.Global.nanoTime(); - message = Message.out(verb, request, nowNanos + DatabaseDescriptor.getAccordRangeBarrierTimeoutNanos()); - } - else - { - message = Message.out(verb, request); + case ACCORD_COMMIT_REQ: + if (((Commit)request).readData == null) + break; + + case ACCORD_READ_REQ: + if (slowRead == null || isRangeBarrier(request)) + break; + + case ACCORD_CHECK_STATUS_REQ: + delayedAtNanos = nowNanos + slowRead.computeWait(1, NANOSECONDS); + break; + + case ACCORD_PRE_ACCEPT_REQ: + if (slowPreaccept == null || isRangeBarrier(request)) + break; + delayedAtNanos = nowNanos + slowPreaccept.computeWait(1, NANOSECONDS); } + + Message message = Message.out(verb, request, expiresAtNanos); InetAddressAndPort endpoint = endpointMapper.mappedEndpoint(to); logger.trace("Sending {} {} to {}", verb, message.payload, endpoint); - messaging.sendWithCallback(message, endpoint, new AccordCallback<>(executor, (Callback) callback, endpointMapper)); + callbacks.registerAt(message.id(), executor, callback, to, nowNanos, delayedAtNanos, expiresAtNanos, NANOSECONDS); + messaging.send(message, endpoint); } @Override diff --git a/src/java/org/apache/cassandra/service/accord/AccordResponseVerbHandler.java b/src/java/org/apache/cassandra/service/accord/AccordResponseVerbHandler.java new file mode 100644 index 0000000000..f5a06f9be8 --- /dev/null +++ b/src/java/org/apache/cassandra/service/accord/AccordResponseVerbHandler.java @@ -0,0 +1,74 @@ +/* + * 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 accord.coordinate.Timeout; +import accord.impl.RequestCallbacks; +import accord.local.Node; +import accord.messages.Reply; +import org.apache.cassandra.exceptions.RequestFailure; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.tracing.Tracing; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime; + +class AccordResponseVerbHandler implements IVerbHandler +{ + private final RequestCallbacks callbacks; + private final AccordEndpointMapper endpointMapper; + + AccordResponseVerbHandler(RequestCallbacks callbacks, AccordEndpointMapper endpointMapper) + { + this.callbacks = callbacks; + this.endpointMapper = endpointMapper; + } + + @Override + public void doVerb(Message message) + { + Node.Id from = endpointMapper.mappedId(message.from()); + if (message.isFailureResponse()) + { + Tracing.trace("Processing failure response from {}", message.from()); + callbacks.onFailure(message.id(), from, convertFailureMessage((RequestFailure) message.payload)); + } + else + { + Tracing.trace("Processing response from {}", message.from()); + boolean remove = !(message.payload instanceof Reply) || ((Reply) message.payload).isFinal(); + RequestCallbacks.CallbackEntry cbe = callbacks.onSuccess(message.id(), from, message.payload, remove); + if (cbe == null) + return; + + long latencyNanos = approxTime.now() - cbe.registeredAt(NANOSECONDS); + MessagingService.instance().latencySubscribers.add(message.from(), latencyNanos, NANOSECONDS); + } + } + + private static Throwable convertFailureMessage(RequestFailure failure) + { + return failure.reason == RequestFailureReason.TIMEOUT ? + new Timeout(null, null) : + new RuntimeException(failure.failure); + } + +} diff --git a/src/java/org/apache/cassandra/service/accord/AccordSafeCommandStore.java b/src/java/org/apache/cassandra/service/accord/AccordSafeCommandStore.java index 624fcc378b..f97f8336b7 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordSafeCommandStore.java +++ b/src/java/org/apache/cassandra/service/accord/AccordSafeCommandStore.java @@ -35,7 +35,7 @@ import accord.impl.AbstractSafeCommandStore; import accord.impl.CommandsSummary; import accord.local.CommandStores; import accord.local.CommandStores.RangesForEpoch; -import accord.local.NodeTimeService; +import accord.local.NodeCommandStoreService; import accord.local.PreLoadContext; import accord.local.RedundantBefore; import accord.local.cfk.CommandsForKey; @@ -179,7 +179,7 @@ public class AccordSafeCommandStore extends AbstractSafeCommandStore implements SegmentCompactor keySupport; - public AccordSegmentCompactor(KeySupport keySupport, int userVersion) + public AccordSegmentCompactor(int userVersion) { this.userVersion = userVersion; - this.keySupport = keySupport; } @Override @@ -148,7 +145,7 @@ public class AccordSegmentCompactor implements SegmentCompactor requestHandler; + private final AccordResponseVerbHandler responseHandler; private final LocalConfig configuration; @GuardedBy("this") @@ -208,7 +209,13 @@ public class AccordService implements IAccordService, Shutdownable private static final IAccordService NOOP_SERVICE = new IAccordService() { @Override - public IVerbHandler verbHandler() + public IVerbHandler requestHandler() + { + return null; + } + + @Override + public IVerbHandler responseHandler() { return null; } @@ -346,10 +353,16 @@ public class AccordService implements IAccordService, Shutdownable return instance != null; } - public static IVerbHandler verbHandlerOrNoop() + public static IVerbHandler requestHandlerOrNoop() { if (!isSetup()) return ignore -> {}; - return instance().verbHandler(); + return instance().requestHandler(); + } + + public static IVerbHandler responseHandlerOrNoop() + { + if (!isSetup()) return ignore -> {}; + return instance().responseHandler(); } public synchronized static void startup(NodeId tcmId) @@ -395,20 +408,17 @@ public class AccordService implements IAccordService, Shutdownable return i; } - public static long now() - { - return TimeUnit.MILLISECONDS.toMicros(Clock.Global.currentTimeMillis()); - } - private AccordService(Id localId) { Invariants.checkState(localId != null, "static localId must be set before instantiating AccordService"); logger.info("Starting accord with nodeId {}", localId); AccordAgent agent = FBUtilities.construct(CassandraRelevantProperties.ACCORD_AGENT_CLASS.getString(AccordAgent.class.getName()), "AccordAgent"); agent.setNodeId(localId); + AccordTimeService time = new AccordTimeService(); + final RequestCallbacks callbacks = new RequestCallbacks(time); this.configService = new AccordConfigurationService(localId); this.fastPathCoordinator = AccordFastPathCoordinator.create(localId, configService); - this.messageSink = new AccordMessageSink(agent, configService); + this.messageSink = new AccordMessageSink(agent, configService, callbacks); this.scheduler = new AccordScheduler(); this.dataStore = new AccordDataStore(); this.configuration = new AccordConfiguration(DatabaseDescriptor.getRawConfig()); @@ -416,8 +426,7 @@ public class AccordService implements IAccordService, Shutdownable this.node = new Node(localId, messageSink, configService, - AccordService::now, - NodeTimeService.elapsedWrapperFromMonotonicSource(NANOSECONDS, Clock.Global::nanoTime), + time, () -> dataStore, new KeyspaceSplitter(new EvenSplit<>(DatabaseDescriptor.getAccordShardCount(), getPartitioner().accordSplitter())), agent, @@ -426,7 +435,7 @@ public class AccordService implements IAccordService, Shutdownable CompositeTopologySorter.create(SizeOfIntersectionSorter.SUPPLIER, new AccordTopologySorter.Supplier(configService, DatabaseDescriptor.getNodeProximity())), DefaultRemoteListeners::new, - DefaultRequestTimeouts::new, + ignore -> callbacks, DefaultProgressLogs::new, DefaultLocalListeners.Factory::new, AccordCommandStores.factory(journal), @@ -436,6 +445,7 @@ public class AccordService implements IAccordService, Shutdownable this.nodeShutdown = toShutdownable(node); this.durabilityScheduling = new CoordinateDurabilityScheduling(node); this.requestHandler = new AccordVerbHandler<>(node, configService); + this.responseHandler = new AccordResponseVerbHandler<>(callbacks, configService); } @Override @@ -568,11 +578,17 @@ public class AccordService implements IAccordService, Shutdownable } @Override - public IVerbHandler verbHandler() + public IVerbHandler requestHandler() { return requestHandler; } + @Override + public IVerbHandler responseHandler() + { + return responseHandler; + } + private Seekables barrier(@Nonnull Seekables keysOrRanges, long epoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite, BiFunction, AsyncSyncPoint> syncPoint) { Stopwatch sw = Stopwatch.createStarted(); diff --git a/src/java/org/apache/cassandra/service/accord/AccordStateCache.java b/src/java/org/apache/cassandra/service/accord/AccordStateCache.java index 8439a05f2e..019b888ded 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordStateCache.java +++ b/src/java/org/apache/cassandra/service/accord/AccordStateCache.java @@ -291,7 +291,6 @@ public class AccordStateCache extends IntrusiveLinkedList validateFunction; private final ToLongFunction heapEstimator; private long bytesCached; -// private int itemsCached; @VisibleForTesting final CacheAccessMetrics instanceMetrics; @@ -382,7 +381,6 @@ public class AccordStateCache extends IntrusiveLinkedList node = (AccordCachingState) cache.get(key); diff --git a/src/java/org/apache/cassandra/service/accord/AccordVerbHandler.java b/src/java/org/apache/cassandra/service/accord/AccordVerbHandler.java index 34c7b26bd9..59fc056b90 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordVerbHandler.java +++ b/src/java/org/apache/cassandra/service/accord/AccordVerbHandler.java @@ -60,7 +60,7 @@ public class AccordVerbHandler implements IVerbHandler Node.Id fromNodeId = endpointMapper.mappedId(message.from()); long waitForEpoch = request.waitForEpoch(); - if (node.topology().hasEpoch(waitForEpoch)) + if (node.topology().hasAtLeastEpoch(waitForEpoch)) request.process(node, fromNodeId, message); else node.withEpoch(waitForEpoch, (ignored, withEpochFailure) -> { diff --git a/src/java/org/apache/cassandra/service/accord/CommandsForRangesLoader.java b/src/java/org/apache/cassandra/service/accord/CommandsForRangesLoader.java index 6b564b7573..02e492f3c6 100644 --- a/src/java/org/apache/cassandra/service/accord/CommandsForRangesLoader.java +++ b/src/java/org/apache/cassandra/service/accord/CommandsForRangesLoader.java @@ -35,11 +35,11 @@ import accord.local.Command; import accord.local.KeyHistory; import accord.local.RedundantBefore; import accord.primitives.PartialDeps; +import accord.primitives.Routable.Domain; import accord.primitives.SaveStatus; import accord.primitives.Status; import accord.primitives.Range; import accord.primitives.Ranges; -import accord.primitives.Routable; import accord.primitives.Routables; import accord.primitives.Seekables; import accord.primitives.Timestamp; @@ -54,16 +54,34 @@ import org.apache.cassandra.utils.Pair; import static accord.primitives.Txn.Kind.ExclusiveSyncPoint; -public class CommandsForRangesLoader +public class CommandsForRangesLoader implements AccordStateCache.Listener { private final RoutesSearcher searcher = new RoutesSearcher(); //TODO (now, durability): find solution for this... private final NavigableMap historicalTransaction = new TreeMap<>(); private final AccordCommandStore store; + private final ObjectHashSet cachedRangeTxns = new ObjectHashSet<>(); public CommandsForRangesLoader(AccordCommandStore store) { this.store = store; + store.commandCache().register(this); + } + + @Override + public void onAdd(AccordCachingState state) + { + TxnId txnId = state.key(); + if (txnId.is(Domain.Range)) + cachedRangeTxns.add(txnId); + } + + @Override + public void onEvict(AccordCachingState state) + { + TxnId txnId = state.key(); + if (txnId.is(Domain.Range)) + cachedRangeTxns.remove(txnId); } public AsyncResult>> get(@Nullable TxnId primaryTxnId, KeyHistory keyHistory, Ranges ranges) @@ -136,7 +154,7 @@ public class CommandsForRangesLoader @Override public void onAdd(AccordCachingState n) { - if (n.key().domain() != Routable.Domain.Range) + if (n.key().domain() != Domain.Range) return; if (n.key().compareTo(minTxnId) < 0 || n.key().compareTo(maxTxnId) >= 0) @@ -197,7 +215,8 @@ public class CommandsForRangesLoader private Watcher fromCache(@Nullable TxnId findAsDep, Ranges ranges, TxnId minTxnId, Timestamp maxTxnId, RedundantBefore redundantBefore) { Watcher watcher = new Watcher(ranges, findAsDep, minTxnId, maxTxnId, redundantBefore); - store.commandCache().stream().forEach(watcher::onAdd); + for (TxnId rangeTxnId : cachedRangeTxns) + watcher.onAdd(store.commandCache().getUnsafe(rangeTxnId)); store.commandCache().register(watcher); return watcher; } @@ -234,11 +253,12 @@ public class CommandsForRangesLoader return null; Seekables> keysOrRanges = cmd.partialTxn().keys(); - if (keysOrRanges.domain() != Routable.Domain.Range) + if (keysOrRanges.domain() != Domain.Range) throw new AssertionError(String.format("Txn keys are not range for %s", cmd.partialTxn())); Ranges ranges = (Ranges) keysOrRanges; - if (!ranges.intersects(cacheRanges)) + ranges = ranges.slice(cacheRanges, Routables.Slice.Minimal); + if (ranges.isEmpty()) return null; if (redundantBefore != null) diff --git a/src/java/org/apache/cassandra/service/accord/IAccordService.java b/src/java/org/apache/cassandra/service/accord/IAccordService.java index e5e2d125f1..bba67a9160 100644 --- a/src/java/org/apache/cassandra/service/accord/IAccordService.java +++ b/src/java/org/apache/cassandra/service/accord/IAccordService.java @@ -34,6 +34,7 @@ import accord.local.DurableBefore; import accord.local.Node; import accord.local.Node.Id; import accord.local.RedundantBefore; +import accord.messages.Reply; import accord.messages.Request; import accord.primitives.Ranges; import accord.primitives.Seekables; @@ -68,7 +69,8 @@ public interface IAccordService Set SUPPORTED_COMMIT_CONSISTENCY_LEVELS = ImmutableSet.of(ConsistencyLevel.ANY, ConsistencyLevel.ONE, ConsistencyLevel.LOCAL_ONE, ConsistencyLevel.QUORUM, ConsistencyLevel.SERIAL, ConsistencyLevel.ALL); Set SUPPORTED_READ_CONSISTENCY_LEVELS = ImmutableSet.of(ConsistencyLevel.ONE, ConsistencyLevel.QUORUM, ConsistencyLevel.SERIAL); - IVerbHandler verbHandler(); + IVerbHandler requestHandler(); + IVerbHandler responseHandler(); Seekables barrierWithRetries(Seekables keysOrRanges, long minEpoch, BarrierType barrierType, boolean isForWrite) throws InterruptedException; diff --git a/src/java/org/apache/cassandra/service/accord/JournalKey.java b/src/java/org/apache/cassandra/service/accord/JournalKey.java index b8b09ef236..99e068ca26 100644 --- a/src/java/org/apache/cassandra/service/accord/JournalKey.java +++ b/src/java/org/apache/cassandra/service/accord/JournalKey.java @@ -71,13 +71,13 @@ public final class JournalKey public static final class JournalKeySupport implements KeySupport { - private static final int MSB_OFFSET = 0; + private static final int CS_ID_OFFSET = 0; + private static final int TYPE_OFFSET = INT_SIZE; + private static final int MSB_OFFSET = TYPE_OFFSET + BYTE_SIZE; private static final int LSB_OFFSET = MSB_OFFSET + LONG_SIZE; private static final int NODE_OFFSET = LSB_OFFSET + LONG_SIZE; - private static final int TYPE_OFFSET = NODE_OFFSET + INT_SIZE; - private static final int CS_ID_OFFSET = TYPE_OFFSET + BYTE_SIZE; // TODO (required): revisit commandStoreId - this can go arbitrarily high so may want to use vint - public static final int TOTAL_SIZE = CS_ID_OFFSET + INT_SIZE; + public static final int TOTAL_SIZE = NODE_OFFSET + INT_SIZE; @Override public int serializedSize(int userVersion) @@ -88,33 +88,33 @@ public final class JournalKey @Override public void serialize(JournalKey key, DataOutputPlus out, int userVersion) throws IOException { - serializeTxnId(key.id, out); - out.writeByte(key.type.id); out.writeInt(key.commandStoreId); + out.writeByte(key.type.id); + serializeTxnId(key.id, out); } private void serialize(JournalKey key, byte[] out) { - serializeTxnId(key.id, out); - out[TYPE_OFFSET] = (byte) (key.type.id & 0xFF); ByteArrayUtil.putInt(out, CS_ID_OFFSET, key.commandStoreId); + out[TYPE_OFFSET] = (byte) (key.type.id & 0xFF); + serializeTxnId(key.id, out); } @Override public JournalKey deserialize(DataInputPlus in, int userVersion) throws IOException { - TxnId txnId = deserializeTxnId(in); - int type = in.readByte(); int commandStoreId = in.readInt(); + int type = in.readByte(); + TxnId txnId = deserializeTxnId(in); return new JournalKey(txnId, Type.fromId(type), commandStoreId); } @Override public JournalKey deserialize(ByteBuffer buffer, int position, int userVersion) { - TxnId txnId = deserializeTxnId(buffer, position); - int type = buffer.get(position + TYPE_OFFSET); int commandStoreId = buffer.getInt(position + CS_ID_OFFSET); + int type = buffer.get(position + TYPE_OFFSET); + TxnId txnId = deserializeTxnId(buffer, position); return new JournalKey(txnId, Type.fromId(type), commandStoreId); } @@ -159,15 +159,15 @@ public final class JournalKey @Override public int compareWithKeyAt(JournalKey k, ByteBuffer buffer, int position, int userVersion) { - int cmp = compareWithTxnIdAt(k.id, buffer, position); + int commandStoreId = buffer.getInt(position + CS_ID_OFFSET); + int cmp = Integer.compare(k.commandStoreId, commandStoreId); if (cmp != 0) return cmp; byte type = buffer.get(position + TYPE_OFFSET); cmp = Byte.compare((byte) k.type.id, type); if (cmp != 0) return cmp; - int commandStoreId = buffer.getInt(position + CS_ID_OFFSET); - cmp = Integer.compare(k.commandStoreId, commandStoreId); + cmp = compareWithTxnIdAt(k.id, buffer, position); return cmp; } @@ -189,9 +189,9 @@ public final class JournalKey @Override public int compare(JournalKey k1, JournalKey k2) { - int cmp = k1.id.compareTo(k2.id); + int cmp = Integer.compare(k1.commandStoreId, k2.commandStoreId); if (cmp == 0) cmp = Byte.compare((byte) k1.type.id, (byte) k2.type.id); - if (cmp == 0) cmp = Integer.compare(k1.commandStoreId, k2.commandStoreId); + if (cmp == 0) cmp = k1.id.compareTo(k2.id); return cmp; } }; diff --git a/src/java/org/apache/cassandra/service/accord/SavedCommand.java b/src/java/org/apache/cassandra/service/accord/SavedCommand.java index a0cd86bb5b..5a0ae5d8ef 100644 --- a/src/java/org/apache/cassandra/service/accord/SavedCommand.java +++ b/src/java/org/apache/cassandra/service/accord/SavedCommand.java @@ -35,6 +35,7 @@ import accord.local.StoreParticipants; import accord.primitives.Ballot; import accord.primitives.PartialDeps; import accord.primitives.PartialTxn; +import accord.primitives.Route; import accord.primitives.SaveStatus; import accord.primitives.Status; import accord.primitives.Timestamp; @@ -57,24 +58,25 @@ import static accord.primitives.Known.KnownDeps.DepsUnknown; import static accord.primitives.Known.KnownDeps.NoDeps; import static accord.primitives.Status.Durability.NotDurable; import static accord.utils.Invariants.illegalState; +import static org.apache.cassandra.service.accord.SavedCommand.Fields.PARTICIPANTS; public class SavedCommand { // This enum is order-dependent public enum Fields { + PARTICIPANTS, // stored first so we can index it + SAVE_STATUS, + PARTIAL_DEPS, EXECUTE_AT, EXECUTES_AT_LEAST, - SAVE_STATUS, DURABILITY, ACCEPTED, PROMISED, - PARTICIPANTS, - PARTIAL_TXN, - PARTIAL_DEPS, WAITING_ON, + PARTIAL_TXN, WRITES, - CLEANUP + CLEANUP, ; public static final Fields[] FIELDS = values(); @@ -233,7 +235,7 @@ public class SavedCommand flags = collectFlags(before, after, Command::acceptedOrCommitted, false, Fields.ACCEPTED, flags); flags = collectFlags(before, after, Command::promised, false, Fields.PROMISED, flags); - flags = collectFlags(before, after, Command::participants, true, Fields.PARTICIPANTS, flags); + flags = collectFlags(before, after, Command::participants, true, PARTICIPANTS, flags); flags = collectFlags(before, after, Command::partialTxn, false, Fields.PARTIAL_TXN, flags); flags = collectFlags(before, after, Command::partialDeps, false, Fields.PARTIAL_DEPS, flags); @@ -540,7 +542,7 @@ public class SavedCommand } if (participants != null) { - builder.flags = setFieldChanged(Fields.PARTICIPANTS, builder.flags); + builder.flags = setFieldChanged(PARTICIPANTS, builder.flags); builder.participants = participants; } if (includeOutcome && builder.writes != null) @@ -579,6 +581,16 @@ public class SavedCommand } } + public static Route deserializeRouteOrNull(DataInputPlus in, int userVersion) throws IOException + { + int flags = in.readInt(); + + if (!getFieldChanged(PARTICIPANTS, flags) || getFieldIsNull(PARTICIPANTS, flags)) + return null; + + return CommandSerializers.participants.deserializeRouteOnly(in, userVersion); + } + public void serialize(DataOutputPlus out, int userVersion) throws IOException { out.writeInt(flags); diff --git a/src/java/org/apache/cassandra/service/accord/api/AccordTimeService.java b/src/java/org/apache/cassandra/service/accord/api/AccordTimeService.java new file mode 100644 index 0000000000..2f13983ea8 --- /dev/null +++ b/src/java/org/apache/cassandra/service/accord/api/AccordTimeService.java @@ -0,0 +1,47 @@ +/* + * 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.concurrent.TimeUnit; + +import accord.local.TimeService; +import org.apache.cassandra.utils.Clock; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static org.apache.cassandra.utils.Clock.Global.nanoTime; + +public class AccordTimeService implements TimeService +{ + @Override + public long now() + { + return nowMicros(); + } + + public static long nowMicros() + { + return TimeUnit.MILLISECONDS.toMicros(Clock.Global.currentTimeMillis()); + } + + @Override + public long elapsed(TimeUnit unit) + { + return unit.convert(nanoTime(), NANOSECONDS); + } +} diff --git a/src/java/org/apache/cassandra/service/accord/api/AccordTopologySorter.java b/src/java/org/apache/cassandra/service/accord/api/AccordTopologySorter.java index bb70132c04..a9f004b350 100644 --- a/src/java/org/apache/cassandra/service/accord/api/AccordTopologySorter.java +++ b/src/java/org/apache/cassandra/service/accord/api/AccordTopologySorter.java @@ -28,6 +28,10 @@ import accord.topology.ShardSelection; import accord.topology.Topologies; import accord.topology.Topology; import accord.utils.SortedList; +import org.apache.cassandra.gms.ApplicationState; +import org.apache.cassandra.gms.EndpointState; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.gms.VersionedValue; import org.apache.cassandra.locator.DynamicEndpointSnitch; import org.apache.cassandra.locator.Endpoint; import org.apache.cassandra.locator.InetAddressAndPort; @@ -71,8 +75,8 @@ public class AccordTopologySorter implements TopologySorter } private final AccordEndpointMapper mapper; - private final Comparator comparator; + private AccordTopologySorter(AccordEndpointMapper mapper, Comparator comparator) { this.mapper = mapper; @@ -95,6 +99,27 @@ public class AccordTopologySorter implements TopologySorter return comparator.compare(() -> mapper.mappedEndpoint(node1), () -> mapper.mappedEndpoint(node2)); } + @Override + public boolean isFaulty(Node.Id node) + { + InetAddressAndPort ep = mapper.mappedEndpointOrNull(node); + if (ep == null) + return true; + + EndpointState epState = Gossiper.instance.getEndpointStateForEndpoint(ep); + if (epState == null) + return true; + + if (!epState.isAlive()) + return true; + + VersionedValue event = epState.getApplicationState(ApplicationState.SEVERITY); + if (event == null) + return false; + + return Double.parseDouble(event.value) == 0.0; + } + private static class EndpointTuple implements Endpoint { final InetAddressAndPort endpoint; diff --git a/src/java/org/apache/cassandra/service/accord/api/CompositeTopologySorter.java b/src/java/org/apache/cassandra/service/accord/api/CompositeTopologySorter.java index 3886cde12d..597e4aad86 100644 --- a/src/java/org/apache/cassandra/service/accord/api/CompositeTopologySorter.java +++ b/src/java/org/apache/cassandra/service/accord/api/CompositeTopologySorter.java @@ -81,4 +81,15 @@ public class CompositeTopologySorter implements TopologySorter } return 0; } + + @Override + public boolean isFaulty(Node.Id node) + { + for (int i = 0; i < delegates.length; i++) + { + if (delegates[i].isFaulty(node)) + return true; + } + return false; + } } diff --git a/src/java/org/apache/cassandra/service/accord/repair/RepairSyncPointAdapter.java b/src/java/org/apache/cassandra/service/accord/repair/RepairSyncPointAdapter.java index 58c9f4b65f..767e57fd9f 100644 --- a/src/java/org/apache/cassandra/service/accord/repair/RepairSyncPointAdapter.java +++ b/src/java/org/apache/cassandra/service/accord/repair/RepairSyncPointAdapter.java @@ -61,7 +61,7 @@ public class RepairSyncPointAdapter extends CoordinationAd public void execute(Node node, Topologies all, FullRoute route, ExecutePath path, TxnId txnId, Txn txn, Timestamp executeAt, Deps deps, BiConsumer, Throwable> callback) { RequiredResponseTracker tracker = new RequiredResponseTracker(requiredResponses, all); - ExecuteSyncPoint.ExecuteBlocking execute = new ExecuteSyncPoint.ExecuteBlocking<>(node, new SyncPoint(txnId, deps, (FullRoute) route), tracker, executeAt); + ExecuteSyncPoint.ExecuteBlocking execute = new ExecuteSyncPoint.ExecuteBlocking<>(node, new SyncPoint<>(txnId, deps, (FullRoute) route), tracker, executeAt); execute.addCallback(callback); execute.start(); } diff --git a/src/java/org/apache/cassandra/service/accord/repair/RequiredResponseTracker.java b/src/java/org/apache/cassandra/service/accord/repair/RequiredResponseTracker.java index 130e914969..ac2651dcd3 100644 --- a/src/java/org/apache/cassandra/service/accord/repair/RequiredResponseTracker.java +++ b/src/java/org/apache/cassandra/service/accord/repair/RequiredResponseTracker.java @@ -21,9 +21,9 @@ package org.apache.cassandra.service.accord.repair; import java.util.HashSet; import java.util.Set; -import accord.coordinate.tracking.AbstractSimpleTracker; import accord.coordinate.tracking.RequestStatus; import accord.coordinate.tracking.ShardTracker; +import accord.coordinate.tracking.SimpleTracker; import accord.local.Node; import accord.topology.Shard; import accord.topology.Topologies; @@ -32,7 +32,7 @@ import static accord.coordinate.tracking.AbstractTracker.ShardOutcomes.Fail; import static accord.coordinate.tracking.AbstractTracker.ShardOutcomes.NoChange; import static accord.coordinate.tracking.AbstractTracker.ShardOutcomes.Success; -public class RequiredResponseTracker extends AbstractSimpleTracker +public class RequiredResponseTracker extends SimpleTracker { public static class RequiredResponseShardTracker extends ShardTracker { diff --git a/src/java/org/apache/cassandra/service/accord/serializers/ReadDataSerializers.java b/src/java/org/apache/cassandra/service/accord/serializers/ReadDataSerializers.java index 6ef51b957d..c537434d95 100644 --- a/src/java/org/apache/cassandra/service/accord/serializers/ReadDataSerializers.java +++ b/src/java/org/apache/cassandra/service/accord/serializers/ReadDataSerializers.java @@ -145,7 +145,7 @@ public class ReadDataSerializers } }; - private static final ReadDataSerializer readEphemeralTxnData = new ReadDataSerializer() + public static final ReadDataSerializer readEphemeralTxnData = new ReadDataSerializer<>() { @Override public void serialize(ReadEphemeralTxnData read, DataOutputPlus out, int version) throws IOException diff --git a/src/java/org/apache/cassandra/service/paxos/ContentionStrategy.java b/src/java/org/apache/cassandra/service/paxos/ContentionStrategy.java index 59ee550512..e513bd1a17 100644 --- a/src/java/org/apache/cassandra/service/paxos/ContentionStrategy.java +++ b/src/java/org/apache/cassandra/service/paxos/ContentionStrategy.java @@ -19,347 +19,83 @@ package org.apache.cassandra.service.paxos; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; -import com.codahale.metrics.Snapshot; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.RetryStrategy; +import org.apache.cassandra.service.TimeoutStrategy; +import org.apache.cassandra.service.TimeoutStrategy.LatencySourceFactory; +import org.apache.cassandra.service.TimeoutStrategy.ReadWriteLatencySourceFactory; +import org.apache.cassandra.service.TimeoutStrategy.Wait; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.utils.ByteBufferUtil; -import org.apache.cassandra.utils.NoSpamLogger; + +import java.util.function.Supplier; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.DoubleSupplier; -import java.util.function.LongBinaryOperator; -import java.util.function.Supplier; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static java.lang.Double.parseDouble; -import static java.lang.Integer.parseInt; import static java.lang.Math.*; -import static java.util.Arrays.stream; import static java.util.concurrent.TimeUnit.*; import static org.apache.cassandra.config.DatabaseDescriptor.*; import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.casReadMetrics; import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.casWriteMetrics; -import static org.apache.cassandra.utils.Clock.Global.nanoTime; import static org.apache.cassandra.utils.Clock.waitUntil; import static org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized; /** - *

    A strategy for making back-off decisions for Paxos operations that fail to make progress because of other paxos operations. - * The strategy is defined by four factors:

      - *
    • {@link #min} - *
    • {@link #max} - *
    • {@link #minDelta} - *
    • {@link #waitRandomizer} - *
    - * - *

    The first three represent time periods, and may be defined dynamically based on a simple calculation over:

      - *
    • {@code pX()} recent experienced latency distribution for successful operations, - * e.g. {@code p50(rw)} the maximum of read and write median latencies, - * {@code p999(r)} the 99.9th percentile of read latencies - *
    • {@code attempts} the number of failed attempts made by the operation so far - *
    • {@code constant} a user provided floating point constant - *
    - * - *

    Their calculation may take any of these forms - *

  • constant {@code $constant$[mu]s} - *
  • dynamic constant {@code pX() * constant} - *
  • dynamic linear {@code pX() * constant * attempts} - *
  • dynamic exponential {@code pX() * constant ^ attempts} - * - *

    Furthermore, the dynamic calculations can be bounded with a min/max, like so: - * {@code min[mu]s <= dynamic expr <= max[mu]s} - * - * e.g. - *

  • {@code 10ms <= p50(rw)*0.66} - *
  • {@code 10ms <= p95(rw)*1.8^attempts <= 100ms} - *
  • {@code 5ms <= p50(rw)*0.5} - * - *

    These calculations are put together to construct a range from which we draw a random number. - * The period we wait for {@code X} will be drawn so that {@code min <= X < max}. - * - *

    With the constraint that {@code max} must be {@code minDelta} greater than {@code min}, - * but no greater than its expression-defined maximum. {@code max} will be increased up until - * this point, after which {@code min} will be decreased until this gap is imposed. - * - *

    The {@link #waitRandomizer} property specifies the manner in which a random value is drawn from the range. - * It is defined using one of the following specifiers: - *

  • uniform - *
  • exp($power$) or exponential($power$) - *
  • qexp($power$) or qexponential($power$) or quantizedexponential($power$) - * - * The uniform specifier is self-explanatory, selecting all values in the range with equal probability. - * The exponential specifier draws values towards the end of the range with higher probability, raising - * a floating point number in the range [0..1.0) to the power provided, and translating the resulting value - * to a uniform value in the range. - * The quantized exponential specifier partitions the range into {@code attempts} buckets, then applies the pure - * exponential approach to draw values from [0..attempts), before drawing a uniform value from the corresponding bucket - * - *

    Finally, there is also a {@link #traceAfterAttempts} property that permits initiating tracing of operations - * that experience a certain minimum number of failed paxos rounds due to contention. A setting of 0 or 1 will initiate - * a trace session after the first failed ballot. + * See {@link RetryStrategy} */ -public class ContentionStrategy +public class ContentionStrategy extends RetryStrategy { - private static final Logger logger = LoggerFactory.getLogger(ContentionStrategy.class); + private static final Logger logger = LoggerFactory.getLogger(RetryStrategy.class); - private static final Pattern BOUND = Pattern.compile( - "(?0|[0-9]+[mu]s)" + - "|((?0|[0-9]+[mu]s) *<= *)?" + - "(p(?[0-9]+)\\((?r|w|rw|wr)\\)|(?0|[0-9]+[mu]s))" + - "\\s*([*]\\s*(?[0-9.]+)?\\s*(?[*^]\\s*attempts)?)?" + - "( *<= *(?0|[0-9]+[mu]s))?"); - private static final Pattern TIME = Pattern.compile( - "0|([0-9]+)ms|([0-9]+)us"); - private static final Pattern RANDOMIZER = Pattern.compile( - "uniform|exp(onential)?[(](?[0-9.]+)[)]|q(uantized)?exp(onential)?[(](?[0-9.]+)[)]"); - private static final String DEFAULT_WAIT_RANDOMIZER = "qexp(1.5)"; // at least 0ms, and at least 66% of median latency - private static final String DEFAULT_MIN = "0 <= p50(rw)*0.66"; // at least 0ms, and at least 66% of median latency - private static final String DEFAULT_MAX = "10ms <= p95(rw)*1.8^attempts <= 100ms"; // p95 latency with exponential back-off at rate of 1.8^attempts - private static final String DEFAULT_MIN_DELTA = "5ms <= p50(rw)*0.5"; // at least 5ms, and at least 50% of median latency + private static final String DEFAULT_WAIT_RANDOMIZER = "uniform"; + private static final String DEFAULT_MIN = "0"; + private static final String DEFAULT_MAX = "100ms"; + private static final String DEFAULT_SPREAD = "100ms"; + private static final LatencySourceFactory LATENCIES = new ReadWriteLatencySourceFactory(casReadMetrics, casWriteMetrics); private static volatile ContentionStrategy current; - - // Factories can be useful for testing purposes, to supply custom implementations of selectors and modifiers. - final static LatencySelectorFactory selectors = new LatencySelectorFactory(){}; - final static LatencyModifierFactory modifiers = new LatencyModifierFactory(){}; - final static WaitRandomizerFactory randomizers = new WaitRandomizerFactory(){}; - + private static volatile ParsedStrategy currentParsed; + private static final RetryStrategy.ParsedStrategy defaultStrategy; static { - current = new ContentionStrategy(defaultWaitRandomizer(), defaultMinWait(), defaultMaxWait(), defaultMinDelta(), Integer.MAX_VALUE); + defaultStrategy = new ParsedStrategy(DEFAULT_WAIT_RANDOMIZER, DEFAULT_MIN, DEFAULT_MAX, DEFAULT_SPREAD, Integer.MAX_VALUE, + new ContentionStrategy(DEFAULT_WAIT_RANDOMIZER, DEFAULT_MIN, DEFAULT_MAX, DEFAULT_SPREAD, Integer.MAX_VALUE)); + + String waitRandomizer = orElse(DatabaseDescriptor::getPaxosContentionWaitRandomizer, DEFAULT_WAIT_RANDOMIZER); + String min = orElse(DatabaseDescriptor::getPaxosContentionMinWait, DEFAULT_MIN); + String max = orElse(DatabaseDescriptor::getPaxosContentionMaxWait, DEFAULT_MAX); + String spread = orElse(DatabaseDescriptor::getPaxosContentionMinDelta, DEFAULT_SPREAD); + + current = new ContentionStrategy(waitRandomizer, min, max, spread, Integer.MAX_VALUE); + currentParsed = new ParsedStrategy(waitRandomizer, min, max, spread, Integer.MAX_VALUE, current); } - static interface LatencyModifierFactory - { - default LatencyModifier identity() { return (l, a) -> l; } - default LatencyModifier multiply(double constant) { return (l, a) -> saturatedCast(l * constant); } - default LatencyModifier multiplyByAttempts(double multiply) { return (l, a) -> saturatedCast(l * multiply * a); } - default LatencyModifier multiplyByAttemptsExp(double base) { return (l, a) -> saturatedCast(l * pow(base, a)); } - } - - static interface LatencySupplier - { - abstract long get(double percentile); - } - - static interface LatencySelector - { - abstract long select(LatencySupplier readLatencyHistogram, LatencySupplier writeLatencyHistogram); - } - - static interface LatencySelectorFactory - { - default LatencySelector constant(long latency) { return (read, write) -> latency; } - default LatencySelector read(double percentile) { return (read, write) -> read.get(percentile); } - default LatencySelector write(double percentile) { return (read, write) -> write.get(percentile); } - default LatencySelector maxReadWrite(double percentile) { return (read, write) -> max(read.get(percentile), write.get(percentile)); } - } - - static interface LatencyModifier - { - long modify(long latency, int attempts); - } - - static interface WaitRandomizer - { - abstract long wait(long min, long max, int attempts); - } - - static interface WaitRandomizerFactory - { - default LongBinaryOperator uniformLongSupplier() { return (min, max) -> ThreadLocalRandom.current().nextLong(min, max); } // DO NOT USE METHOD HANDLES (want to fetch afresh each time) - default DoubleSupplier uniformDoubleSupplier() { return () -> ThreadLocalRandom.current().nextDouble(); } - - default WaitRandomizer uniform() { return new Uniform(uniformLongSupplier()); } - default WaitRandomizer exponential(double power) { return new Exponential(uniformLongSupplier(), uniformDoubleSupplier(), power); } - default WaitRandomizer quantizedExponential(double power) { return new QuantizedExponential(uniformLongSupplier(), uniformDoubleSupplier(), power); } - - static class Uniform implements WaitRandomizer - { - final LongBinaryOperator uniformLong; - - public Uniform(LongBinaryOperator uniformLong) - { - this.uniformLong = uniformLong; - } - - @Override - public long wait(long min, long max, int attempts) - { - return uniformLong.applyAsLong(min, max); - } - } - - static abstract class AbstractExponential implements WaitRandomizer - { - final LongBinaryOperator uniformLong; - final DoubleSupplier uniformDouble; - final double power; - - public AbstractExponential(LongBinaryOperator uniformLong, DoubleSupplier uniformDouble, double power) - { - this.uniformLong = uniformLong; - this.uniformDouble = uniformDouble; - this.power = power; - } - } - - static class Exponential extends AbstractExponential - { - public Exponential(LongBinaryOperator uniformLong, DoubleSupplier uniformDouble, double power) - { - super(uniformLong, uniformDouble, power); - } - - @Override - public long wait(long min, long max, int attempts) - { - if (attempts == 1) - return uniformLong.applyAsLong(min, max); - - double p = uniformDouble.getAsDouble(); - long delta = max - min; - delta *= Math.pow(p, power); - return max - delta; - } - } - - static class QuantizedExponential extends AbstractExponential - { - public QuantizedExponential(LongBinaryOperator uniformLong, DoubleSupplier uniformDouble, double power) - { - super(uniformLong, uniformDouble, power); - } - - @Override - public long wait(long min, long max, int attempts) - { - long quanta = (max - min) / attempts; - if (attempts == 1 || quanta == 0) - return uniformLong.applyAsLong(min, max); - - double p = uniformDouble.getAsDouble(); - int base = (int) (attempts * Math.pow(p, power)); - return max - ThreadLocalRandom.current().nextLong(quanta * base, quanta * (base + 1)); - } - } - } - - static class SnapshotAndTime - { - final long validUntil; - final Snapshot snapshot; - - SnapshotAndTime(long validUntil, Snapshot snapshot) - { - this.validUntil = validUntil; - this.snapshot = snapshot; - } - } - - static class TimeLimitedLatencySupplier extends AtomicReference implements LatencySupplier - { - final Supplier snapshotSupplier; - final long validForNanos; - - TimeLimitedLatencySupplier(Supplier snapshotSupplier, long time, TimeUnit units) - { - this.snapshotSupplier = snapshotSupplier; - this.validForNanos = units.toNanos(time); - } - - private Snapshot getSnapshot() - { - long now = nanoTime(); - - SnapshotAndTime cur = get(); - if (cur != null && cur.validUntil > now) - return cur.snapshot; - - Snapshot newSnapshot = snapshotSupplier.get(); - SnapshotAndTime next = new SnapshotAndTime(now + validForNanos, newSnapshot); - if (compareAndSet(cur, next)) - return next.snapshot; - - return accumulateAndGet(next, (a, b) -> a.validUntil > b.validUntil ? a : b).snapshot; - } - - @Override - public long get(double percentile) - { - return (long)getSnapshot().getValue(percentile); - } - } - - static class Bound - { - final long min, max, onFailure; - final LatencyModifier modifier; - final LatencySelector selector; - final LatencySupplier reads, writes; - - Bound(long min, long max, long onFailure, LatencyModifier modifier, LatencySelector selector) - { - Preconditions.checkArgument(min<=max, "min (%s) must be less than or equal to max (%s)", min, max); - this.min = min; - this.max = max; - this.onFailure = onFailure; - this.modifier = modifier; - this.selector = selector; - this.reads = new TimeLimitedLatencySupplier(casReadMetrics.latency::getSnapshot, 10L, SECONDS); - this.writes = new TimeLimitedLatencySupplier(casWriteMetrics.latency::getSnapshot, 10L, SECONDS); - } - - long get(int attempts) - { - try - { - long base = selector.select(reads, writes); - return max(min, min(max, modifier.modify(base, attempts))); - } - catch (Throwable t) - { - NoSpamLogger.getLogger(logger, 1L, MINUTES).info("", t); - return onFailure; - } - } - - public String toString() - { - return "Bound{" + - "min=" + min + - ", max=" + max + - ", onFailure=" + onFailure + - ", modifier=" + modifier + - ", selector=" + selector + - '}'; - } - } - - final WaitRandomizer waitRandomizer; - final Bound min, max, minDelta; final int traceAfterAttempts; - public ContentionStrategy(String waitRandomizer, String min, String max, String minDelta, int traceAfterAttempts) + public ContentionStrategy(String waitRandomizer, String min, String max, String spread, int traceAfterAttempts) { - this.waitRandomizer = parseWaitRandomizer(waitRandomizer); - this.min = parseBound(min, true); - this.max = parseBound(max, false); - this.minDelta = parseBound(minDelta, true); + super(waitRandomizer, min, max, spread, LATENCIES); this.traceAfterAttempts = traceAfterAttempts; } + public ContentionStrategy(WaitRandomizer waitRandomizer, Wait min, Wait max, Wait spread, int traceAfterAttempts) + { + super(waitRandomizer, min, max, spread); + this.traceAfterAttempts = traceAfterAttempts; + } + + @Override + protected Wait parseBound(String spec, boolean isMin, LatencySourceFactory latencies) + { + return TimeoutStrategy.parseWait(spec, 0, maxQueryTimeoutMicros(), isMin ? 0 : maxQueryTimeoutMicros(), latencies); + } + public enum Type { READ("Contended Paxos Read"), WRITE("Contended Paxos Write"), REPAIR("Contended Paxos Repair"); @@ -395,25 +131,10 @@ public class ContentionStrategy Tracing.instance.getSessionId()); } - long minWaitMicros = min.get(attempts); - long maxWaitMicros = max.get(attempts); - long minDeltaMicros = minDelta.get(attempts); - - if (minWaitMicros + minDeltaMicros > maxWaitMicros) - { - maxWaitMicros = minWaitMicros + minDeltaMicros; - if (maxWaitMicros > this.max.max) - { - maxWaitMicros = this.max.max; - minWaitMicros = max(this.min.min, min(this.min.max, maxWaitMicros - minDeltaMicros)); - } - } - - long wait = waitRandomizer.wait(minWaitMicros, maxWaitMicros, attempts); - return nanoTime() + MICROSECONDS.toNanos(wait); + return super.computeWaitUntil(attempts); } - boolean doWaitForContention(long deadline, int attempts, TableMetadata table, DecoratedKey partitionKey, ConsistencyLevel consistency, Type type) + public boolean doWaitForContention(long deadline, int attempts, TableMetadata table, DecoratedKey partitionKey, ConsistencyLevel consistency, Type type) { long until = computeWaitUntilForContention(attempts, table, partitionKey, consistency, type); if (until >= deadline) @@ -441,201 +162,52 @@ public class ContentionStrategy return current.computeWaitUntilForContention(attempts, table, partitionKey, consistency, type); } - static class ParsedStrategy + public static class ParsedStrategy extends RetryStrategy.ParsedStrategy { - final String waitRandomizer, min, max, minDelta; - final ContentionStrategy strategy; + public final int trace; + public final ContentionStrategy strategy; - ParsedStrategy(String waitRandomizer, String min, String max, String minDelta, ContentionStrategy strategy) + ParsedStrategy(String waitRandomizer, String min, String max, String minDelta, int trace, ContentionStrategy strategy) { - this.waitRandomizer = waitRandomizer; - this.min = min; - this.max = max; - this.minDelta = minDelta; + super(waitRandomizer, min, max, minDelta, strategy); + this.trace = trace; this.strategy = strategy; } + + @Override + public String toString() + { + return super.toString() + (trace == Integer.MAX_VALUE ? "" : ",trace=" + current.traceAfterAttempts); + } } @VisibleForTesting - static ParsedStrategy parseStrategy(String spec) + public static ParsedStrategy parseStrategy(String spec) { + RetryStrategy.ParsedStrategy parsed = RetryStrategy.parseStrategy(spec, LATENCIES, defaultStrategy); String[] args = spec.split(","); - String waitRandomizer = find(args, "random"); - String min = find(args, "min"); - String max = find(args, "max"); - String minDelta = find(args, "delta"); - String trace = find(args, "trace"); - if (waitRandomizer == null) waitRandomizer = defaultWaitRandomizer(); - if (min == null) min = defaultMinWait(); - if (max == null) max = defaultMaxWait(); - if (minDelta == null) minDelta = defaultMinDelta(); + String trace = find(args, "trace"); int traceAfterAttempts = trace == null ? current.traceAfterAttempts: Integer.parseInt(trace); - ContentionStrategy strategy = new ContentionStrategy(waitRandomizer, min, max, minDelta, traceAfterAttempts); - return new ParsedStrategy(waitRandomizer, min, max, minDelta, strategy); + ContentionStrategy strategy = new ContentionStrategy(parsed.strategy.waitRandomizer, parsed.strategy.min, parsed.strategy.max, parsed.strategy.spread, traceAfterAttempts); + return new ParsedStrategy(parsed.waitRandomizer, parsed.min, parsed.max, parsed.spread, traceAfterAttempts, strategy); } - - public static void setStrategy(String spec) + public static synchronized void setStrategy(String spec) { ParsedStrategy parsed = parseStrategy(spec); + currentParsed = parsed; current = parsed.strategy; setPaxosContentionWaitRandomizer(parsed.waitRandomizer); setPaxosContentionMinWait(parsed.min); setPaxosContentionMaxWait(parsed.max); - setPaxosContentionMinDelta(parsed.minDelta); + setPaxosContentionMinDelta(parsed.spread); } public static String getStrategySpec() { - return "min=" + defaultMinWait() - + ",max=" + defaultMaxWait() - + ",delta=" + defaultMinDelta() - + ",random=" + defaultWaitRandomizer() - + ",trace=" + current.traceAfterAttempts; - } - - private static String find(String[] args, String param) - { - return stream(args).filter(s -> s.startsWith(param + '=')) - .map(s -> s.substring(param.length() + 1)) - .findFirst().orElse(null); - } - - private static LatencySelector parseLatencySelector(Matcher m, LatencySelectorFactory selectors) - { - String perc = m.group("perc"); - if (perc == null) - return selectors.constant(parseInMicros(m.group("constbase"))); - - double percentile = parseDouble("0." + perc); - String rw = m.group("rw"); - if (rw.length() == 2) - return selectors.maxReadWrite(percentile); - else if ("r".equals(rw)) - return selectors.read(percentile); - else - return selectors.write(percentile); - } - - private static LatencyModifier parseLatencyModifier(Matcher m, LatencyModifierFactory modifiers) - { - String mod = m.group("mod"); - if (mod == null) - return modifiers.identity(); - - double modifier = parseDouble(mod); - - String modkind = m.group("modkind"); - if (modkind == null) - return modifiers.multiply(modifier); - - if (modkind.startsWith("*")) - return modifiers.multiplyByAttempts(modifier); - else if (modkind.startsWith("^")) - return modifiers.multiplyByAttemptsExp(modifier); - else - throw new IllegalArgumentException("Unrecognised attempt modifier: " + modkind); - } - - static long saturatedCast(double v) - { - if (v > Long.MAX_VALUE) - return Long.MAX_VALUE; - return (long) v; - } - - static WaitRandomizer parseWaitRandomizer(String input) - { - return parseWaitRandomizer(input, randomizers); - } - - static WaitRandomizer parseWaitRandomizer(String input, WaitRandomizerFactory randomizers) - { - Matcher m = RANDOMIZER.matcher(input); - if (!m.matches()) - throw new IllegalArgumentException(input + " does not match" + RANDOMIZER); - - String exp; - exp = m.group("exp"); - if (exp != null) - return randomizers.exponential(Double.parseDouble(exp)); - exp = m.group("qexp"); - if (exp != null) - return randomizers.quantizedExponential(Double.parseDouble(exp)); - return randomizers.uniform(); - } - - static Bound parseBound(String input, boolean isMin) - { - return parseBound(input, isMin, selectors, modifiers); - } - - @VisibleForTesting - static Bound parseBound(String input, boolean isMin, LatencySelectorFactory selectors, LatencyModifierFactory modifiers) - { - Matcher m = BOUND.matcher(input); - if (!m.matches()) - throw new IllegalArgumentException(input + " does not match " + BOUND); - - String maybeConst = m.group("const"); - if (maybeConst != null) - { - long v = parseInMicros(maybeConst); - return new Bound(v, v, v, modifiers.identity(), selectors.constant(v)); - } - - long min = parseInMicros(m.group("min"), 0); - long max = parseInMicros(m.group("max"), maxQueryTimeoutMicros() / 2); - return new Bound(min, max, isMin ? min : max, parseLatencyModifier(m, modifiers), parseLatencySelector(m, selectors)); - } - - private static long parseInMicros(String input, long orElse) - { - if (input == null) - return orElse; - - return parseInMicros(input); - } - - private static long parseInMicros(String input) - { - Matcher m = TIME.matcher(input); - if (!m.matches()) - throw new IllegalArgumentException(input + " does not match " + TIME); - - String text; - if (null != (text = m.group(1))) - return parseInt(text) * 1000; - else if (null != (text = m.group(2))) - return parseInt(text); - else - return 0; - } - - @VisibleForTesting - static String defaultWaitRandomizer() - { - return orElse(DatabaseDescriptor::getPaxosContentionWaitRandomizer, DEFAULT_WAIT_RANDOMIZER); - } - - @VisibleForTesting - static String defaultMinWait() - { - return orElse(DatabaseDescriptor::getPaxosContentionMinWait, DEFAULT_MIN); - } - - @VisibleForTesting - static String defaultMaxWait() - { - return orElse(DatabaseDescriptor::getPaxosContentionMaxWait, DEFAULT_MAX); - } - - @VisibleForTesting - static String defaultMinDelta() - { - return orElse(DatabaseDescriptor::getPaxosContentionMinDelta, DEFAULT_MIN_DELTA); + return currentParsed.toString(); } @VisibleForTesting diff --git a/test/distributed/org/apache/cassandra/distributed/test/ForBenchmarks.java b/test/distributed/org/apache/cassandra/distributed/test/ForBenchmarks.java new file mode 100644 index 0000000000..fc4dc57a7b --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/ForBenchmarks.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.Feature; + +import java.io.IOException; + +public class ForBenchmarks extends TestBaseImpl { + public static void main(String[] args) throws IOException, InterruptedException { + try (Cluster cluster = Cluster.build(3) + .withConfig(c -> c.with(Feature.values())) + .start()) { + cluster.get(1).nodetoolResult("cms", "reconfigure", "3").asserts().success(); + + Thread.currentThread().join(); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordLoadTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordLoadTest.java index d9315cf2c7..d750e86844 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordLoadTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordLoadTest.java @@ -27,6 +27,9 @@ import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -61,7 +64,8 @@ public class AccordLoadTest extends AccordTestBase public static void setUp() throws IOException { CassandraRelevantProperties.SIMULATOR_STARTED.setString(Long.toString(MILLISECONDS.toSeconds(currentTimeMillis()))); - AccordTestBase.setupCluster(builder -> builder, 2); + AccordTestBase.setupCluster(builder -> builder, 3); +// AccordTestBase.setupCluster(builder -> builder.withConfig(config -> config.with(Feature.values())), 3); } @Ignore @@ -71,151 +75,187 @@ public class AccordLoadTest extends AccordTestBase test("CREATE TABLE " + qualifiedAccordTableName + " (k int, v int, PRIMARY KEY(k)) WITH transactional_mode = 'full'", cluster -> { - final ConcurrentHashMap verbs = new ConcurrentHashMap<>(); - cluster.filters().outbound().messagesMatching(new IMessageFilters.Matcher() + try { - @Override - public boolean matches(int i, int i1, IMessage iMessage) + + final ConcurrentHashMap verbs = new ConcurrentHashMap<>(); + cluster.filters().outbound().messagesMatching(new IMessageFilters.Matcher() { - verbs.computeIfAbsent(Verb.fromId(iMessage.verb()), ignore -> new AtomicInteger()).incrementAndGet(); - return false; - } - }).drop(); + @Override + public boolean matches(int i, int i1, IMessage iMessage) + { + verbs.computeIfAbsent(Verb.fromId(iMessage.verb()), ignore -> new AtomicInteger()).incrementAndGet(); + return false; + } + }).drop(); - cluster.forEach(i -> i.runOnInstance(() -> { - ((AccordService) AccordService.instance()).journal().compactor().updateCompactionPeriod(1, SECONDS); -// ((AccordSpec.JournalSpec)((AccordService) AccordService.instance()).journal().configuration()).segmentSize = 128 << 10; - })); + ICoordinator coordinator = cluster.coordinator(1); + final int repairInterval = Integer.MAX_VALUE; + // final int repairInterval = 3000; + final int compactionInterval = Integer.MAX_VALUE; + // final int compactionInterval = 3000; + final int flushInterval = Integer.MAX_VALUE; + // final int flushInterval = 1000; + final int compactionPeriodSeconds = -1; + final int restartInterval = 150_000_000; + final int batchSizeLimit = 1000; + final long batchTime = TimeUnit.SECONDS.toNanos(10); + final int concurrency = 100; + final int ratePerSecond = 1000; + final int keyCount = 1000000; + final float readChance = 0.33f; + long nextRepairAt = repairInterval; + long nextCompactionAt = compactionInterval; + long nextFlushAt = flushInterval; + long nextRestartAt = restartInterval; + final ExecutorService restartExecutor = Executors.newSingleThreadExecutor(); + final BitSet initialised = new BitSet(); - ICoordinator coordinator = cluster.coordinator(1); - final int repairInterval = 3000; - final int compactionInterval = 3000; - final int flushInterval = 1000; - final int batchSizeLimit = 1000; - final long batchTime = TimeUnit.SECONDS.toNanos(10); - final int concurrency = 100; - final int ratePerSecond = 1000; - final int keyCount = 1000000; - final float readChance = 0.33f; - long nextRepairAt = repairInterval; - long nextCompactionAt = compactionInterval; - long nextFlushAt = flushInterval; - final BitSet initialised = new BitSet(); + cluster.get(1).nodetoolResult("cms", "reconfigure", "3").asserts().success(); + cluster.forEach(i -> i.runOnInstance(() -> { + if (compactionPeriodSeconds > 0) + ((AccordService) AccordService.instance()).journal().compactor().updateCompactionPeriod(1, SECONDS); + // ((AccordSpec.JournalSpec)((AccordService) AccordService.instance()).journal().configuration()).segmentSize = 128 << 10; + })); - Random random = new Random(); -// CopyOnWriteArrayList exceptions = new CopyOnWriteArrayList<>(); - final Semaphore inFlight = new Semaphore(concurrency); - final RateLimiter rateLimiter = RateLimiter.create(ratePerSecond); -// long testStart = System.nanoTime(); -// while (NANOSECONDS.toMinutes(System.nanoTime() - testStart) < 10 && exceptions.size() < 10000) - while (true) - { - final EstimatedHistogram histogram = new EstimatedHistogram(200); - long batchStart = System.nanoTime(); - long batchEnd = batchStart + batchTime; - int batchSize = 0; - while (batchSize < batchSizeLimit) + Random random = new Random(); + // CopyOnWriteArrayList exceptions = new CopyOnWriteArrayList<>(); + final Semaphore inFlight = new Semaphore(concurrency); + final RateLimiter rateLimiter = RateLimiter.create(ratePerSecond); + // long testStart = System.nanoTime(); + // while (NANOSECONDS.toMinutes(System.nanoTime() - testStart) < 10 && exceptions.size() < 10000) + while (true) { - inFlight.acquire(); - rateLimiter.acquire(); - long commandStart = System.nanoTime(); - int k = random.nextInt(keyCount); - if (random.nextFloat() < readChance) + final EstimatedHistogram histogram = new EstimatedHistogram(200); + long batchStart = System.nanoTime(); + long batchEnd = batchStart + batchTime; + int batchSize = 0; + while (batchSize < batchSizeLimit) { - coordinator.executeWithResult((success, fail) -> { - inFlight.release(); - if (fail == null) histogram.add(NANOSECONDS.toMicros(System.nanoTime() - commandStart)); - // else exceptions.add(fail); - }, "SELECT * FROM " + qualifiedAccordTableName + " WHERE k = ?;", ConsistencyLevel.SERIAL, k); + inFlight.acquire(); + rateLimiter.acquire(); + long commandStart = System.nanoTime(); + int k = random.nextInt(keyCount); + if (random.nextFloat() < readChance) + { + coordinator.executeWithResult((success, fail) -> { + inFlight.release(); + if (fail == null) histogram.add(NANOSECONDS.toMicros(System.nanoTime() - commandStart)); + // else exceptions.add(fail); + }, "SELECT * FROM " + qualifiedAccordTableName + " WHERE k = ?;", ConsistencyLevel.SERIAL, k); + } + else if (initialised.get(k)) + { + coordinator.executeWithResult((success, fail) -> { + inFlight.release(); + if (fail == null) histogram.add(NANOSECONDS.toMicros(System.nanoTime() - commandStart)); + // else exceptions.add(fail); + }, "UPDATE " + qualifiedAccordTableName + " SET v += 1 WHERE k = ? IF EXISTS;", ConsistencyLevel.SERIAL, ConsistencyLevel.QUORUM, k); + } + else + { + initialised.set(k); + coordinator.executeWithResult((success, fail) -> { + inFlight.release(); + if (fail == null) histogram.add(NANOSECONDS.toMicros(System.nanoTime() - commandStart)); + // else exceptions.add(fail); + }, "UPDATE " + qualifiedAccordTableName + " SET v = 0 WHERE k = ? IF NOT EXISTS;", ConsistencyLevel.SERIAL, ConsistencyLevel.QUORUM, k); + } + batchSize++; + if (System.nanoTime() >= batchEnd) + break; } - else if (initialised.get(k)) - { - coordinator.executeWithResult((success, fail) -> { - inFlight.release(); - if (fail == null) histogram.add(NANOSECONDS.toMicros(System.nanoTime() - commandStart)); - // else exceptions.add(fail); - }, "UPDATE " + qualifiedAccordTableName + " SET v += 1 WHERE k = ? IF EXISTS;", ConsistencyLevel.SERIAL, ConsistencyLevel.QUORUM, k); - } - else - { - initialised.set(k); - coordinator.executeWithResult((success, fail) -> { - inFlight.release(); - if (fail == null) histogram.add(NANOSECONDS.toMicros(System.nanoTime() - commandStart)); - // else exceptions.add(fail); - }, "UPDATE " + qualifiedAccordTableName + " SET v = 0 WHERE k = ? IF NOT EXISTS;", ConsistencyLevel.SERIAL, ConsistencyLevel.QUORUM, k); - } - batchSize++; - if (System.nanoTime() >= batchEnd) - break; - } - if ((nextRepairAt -= batchSize) <= 0) - { - nextRepairAt += repairInterval; - System.out.println("repairing..."); - cluster.coordinator(1).instance().nodetool("repair", qualifiedAccordTableName); - } + if ((nextRepairAt -= batchSize) <= 0) + { + nextRepairAt += repairInterval; + System.out.println("repairing..."); + cluster.coordinator(1).instance().nodetool("repair", qualifiedAccordTableName); + } - if ((nextCompactionAt -= batchSize) <= 0) - { - nextCompactionAt += compactionInterval; - System.out.println("compacting accord..."); - cluster.forEach(i -> { - i.nodetool("compact", "system_accord.journal"); - i.runOnInstance(() -> { - ((AccordService) AccordService.instance()).journal().checkAllCommands(); + if ((nextCompactionAt -= batchSize) <= 0) + { + nextCompactionAt += compactionInterval; + System.out.println("compacting accord..."); + cluster.forEach(i -> { + i.nodetool("compact", "system_accord.journal"); + i.runOnInstance(() -> { + ((AccordService) AccordService.instance()).journal().checkAllCommands(); + }); }); - }); - - } - - if ((nextFlushAt -= batchSize) <= 0) - { - nextFlushAt += flushInterval; - System.out.println("flushing journal..."); - cluster.forEach(i -> i.runOnInstance(() -> { - ((AccordService) AccordService.instance()).journal().closeCurrentSegmentForTestingIfNonEmpty(); - ((AccordService) AccordService.instance()).journal().checkAllCommands(); - })); - } - - final Date date = new Date(); - System.out.printf("%tT rate: %.2f/s (%d total)\n", date, (((float)batchSizeLimit * 1000) / NANOSECONDS.toMillis(System.nanoTime() - batchStart)), batchSize); - System.out.printf("%tT percentiles: %d %d %d %d\n", date, histogram.percentile(.25)/1000, histogram.percentile(.5)/1000, histogram.percentile(.75)/1000, histogram.percentile(1)/1000); - - class VerbCount - { - final Verb verb; - final int count; - - VerbCount(Verb verb, int count) - { - this.verb = verb; - this.count = count; } - } - List verbCounts = new ArrayList<>(); - for (Map.Entry e : verbs.entrySet()) - { - int count = e.getValue().getAndSet(0); - if (count != 0) verbCounts.add(new VerbCount(e.getKey(), count)); - } - verbCounts.sort(Comparator.comparing(v -> -v.count)); - StringBuilder verbSummary = new StringBuilder(); - for (VerbCount vs : verbCounts) - { + if ((nextFlushAt -= batchSize) <= 0) { - if (verbSummary.length() > 0) - verbSummary.append(", "); - verbSummary.append(vs.verb); - verbSummary.append(": "); - verbSummary.append(vs.count); + nextFlushAt += flushInterval; + System.out.println("flushing journal..."); + cluster.forEach(i -> i.runOnInstance(() -> { + ((AccordService) AccordService.instance()).journal().closeCurrentSegmentForTestingIfNonEmpty(); + ((AccordService) AccordService.instance()).journal().checkAllCommands(); + })); } + + if ((nextRestartAt -= batchSize) <= 0) + { + nextRestartAt += restartInterval; + int nodeIdx = random.nextInt(cluster.size()); + + restartExecutor.submit(() -> { + System.out.printf("restarting node %d...\n", nodeIdx); + try + { + cluster.get(nodeIdx).shutdown().get(); + cluster.get(nodeIdx).startup(); + return null; + } + catch (InterruptedException | ExecutionException e) + { + throw new RuntimeException(e); + } + }); + } + + final Date date = new Date(); + System.out.printf("%tT rate: %.2f/s (%d total)\n", date, (((float)batchSizeLimit * 1000) / NANOSECONDS.toMillis(System.nanoTime() - batchStart)), batchSize); + System.out.printf("%tT percentiles: %d %d %d %d\n", date, histogram.percentile(.25)/1000, histogram.percentile(.5)/1000, histogram.percentile(.75)/1000, histogram.percentile(1)/1000); + + class VerbCount + { + final Verb verb; + final int count; + + VerbCount(Verb verb, int count) + { + this.verb = verb; + this.count = count; + } + } + List verbCounts = new ArrayList<>(); + for (Map.Entry e : verbs.entrySet()) + { + int count = e.getValue().getAndSet(0); + if (count != 0) verbCounts.add(new VerbCount(e.getKey(), count)); + } + verbCounts.sort(Comparator.comparing(v -> -v.count)); + + StringBuilder verbSummary = new StringBuilder(); + for (VerbCount vs : verbCounts) + { + { + if (verbSummary.length() > 0) + verbSummary.append(", "); + verbSummary.append(vs.verb); + verbSummary.append(": "); + verbSummary.append(vs.count); + } + } + System.out.printf("%tT verbs: %s\n", date, verbSummary); } - System.out.printf("%tT verbs: %s\n", date, verbSummary); - } + } + catch (Throwable t) + { + t.printStackTrace(); + } } ); } diff --git a/test/distributed/org/apache/cassandra/service/accord/AccordJournalCompactionTest.java b/test/distributed/org/apache/cassandra/service/accord/AccordJournalCompactionTest.java index 1e69174d30..3367c72611 100644 --- a/test/distributed/org/apache/cassandra/service/accord/AccordJournalCompactionTest.java +++ b/test/distributed/org/apache/cassandra/service/accord/AccordJournalCompactionTest.java @@ -117,6 +117,7 @@ public class AccordJournalCompactionTest try { journal.start(null); + journal.unsafeSetStarted(); Timestamp timestamp = Timestamp.NONE; RandomSource rs = new DefaultRandom(1); diff --git a/test/unit/org/apache/cassandra/journal/IndexTest.java b/test/unit/org/apache/cassandra/journal/IndexTest.java index ce5ed00ebe..9b6a69505e 100644 --- a/test/unit/org/apache/cassandra/journal/IndexTest.java +++ b/test/unit/org/apache/cassandra/journal/IndexTest.java @@ -136,8 +136,8 @@ public class IndexTest { assertArrayEquals(EMPTY, onDisk.lookUp(key0)); assertArrayEquals(new long[] { composeOffsetAndSize(val11, 1) }, onDisk.lookUp(key1)); - assertArrayEquals(new long[] { composeOffsetAndSize(val21, 2), composeOffsetAndSize(val22, 3) }, onDisk.lookUp(key2)); - assertArrayEquals(new long[] { composeOffsetAndSize(val31, 4), composeOffsetAndSize(val32, 5), composeOffsetAndSize(val33, 6) }, onDisk.lookUp(key3)); + assertArrayEquals(new long[] { composeOffsetAndSize(val22, 3), composeOffsetAndSize(val21, 2) }, onDisk.lookUp(key2)); + assertArrayEquals(new long[] { composeOffsetAndSize(val33, 6), composeOffsetAndSize(val32, 5), composeOffsetAndSize(val31, 4) }, onDisk.lookUp(key3)); assertArrayEquals(EMPTY, onDisk.lookUp(key4)); assertEquals(key1, onDisk.firstId()); diff --git a/test/unit/org/apache/cassandra/service/RetryStrategyTest.java b/test/unit/org/apache/cassandra/service/RetryStrategyTest.java new file mode 100644 index 0000000000..e0fed7475c --- /dev/null +++ b/test/unit/org/apache/cassandra/service/RetryStrategyTest.java @@ -0,0 +1,482 @@ +///* +// * 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; +// +//import java.util.List; +//import java.util.Random; +//import java.util.concurrent.ThreadLocalRandom; +//import java.util.concurrent.TimeUnit; +//import java.util.concurrent.atomic.AtomicReference; +//import java.util.function.BiFunction; +//import java.util.function.Consumer; +//import java.util.function.DoubleSupplier; +//import java.util.function.LongBinaryOperator; +// +//import com.google.common.collect.ImmutableList; +//import org.junit.Assert; +//import org.junit.Test; +// +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +// +//import net.nicoulaj.compilecommand.annotations.Inline; +//import org.apache.cassandra.config.DatabaseDescriptor; +//import org.apache.cassandra.service.TimeoutStrategy.LatencyModifier; +//import org.apache.cassandra.service.TimeoutStrategy.LatencyModifierFactory; +//import org.apache.cassandra.service.TimeoutStrategy.LatencySource; +//import org.apache.cassandra.service.TimeoutStrategy.LatencySupplierFactory; +//import org.apache.cassandra.service.TimeoutStrategy.LatencySupplier; +//import org.apache.cassandra.service.TimeoutStrategy.Wait; +//import org.apache.cassandra.service.paxos.ContentionStrategy; +// +//import static org.apache.cassandra.service.RetryStrategy.*; +//import static org.apache.cassandra.service.RetryStrategy.WaitRandomizerFactory.*; +//import static org.apache.cassandra.service.RetryStrategyTest.WaitRandomizerType.*; +//import static org.apache.cassandra.service.TimeoutStrategy.modifiers; +//import static org.apache.cassandra.service.TimeoutStrategy.parseWait; +//import static org.apache.cassandra.service.TimeoutStrategy.selectors; +// +//public class RetryStrategyTest +//{ +// private static final Logger logger = LoggerFactory.getLogger(RetryStrategyTest.class); +// +// static +// { +// DatabaseDescriptor.daemonInitialization(); +// } +// +// private static final long MAX = DatabaseDescriptor.getRpcTimeout(TimeUnit.MICROSECONDS); +// +// private static final String DEFAULT_WAIT_RANDOMIZER = "qexp(1.5)"; // at least 0ms, and at least 66% of median latency +// private static final String DEFAULT_MIN = "0 <= p50(rw)*0.66"; // at least 0ms, and at least 66% of median latency +// private static final String DEFAULT_MAX = "10ms <= p95(rw)*1.8^attempts <= 100ms"; // p95 latency with exponential back-off at rate of 1.8^attempts +// private static final String DEFAULT_SPREAD = "5ms <= p50(rw)*0.5"; // at least 5ms, and at least 50% of median latency +// +// private static final WaitRandomizerParseValidator DEFAULT_WAIT_RANDOMIZER_VALIDATOR = new WaitRandomizerParseValidator(DEFAULT_WAIT_RANDOMIZER, QEXP, 1.5); +// private static final WaitParseValidator DEFAULT_MIN_VALIDATOR = new WaitParseValidator(DEFAULT_MIN, true, assertWait(0, MAX, 0, selectors.maxReadWrite(0f).getClass(), 0.50, 0, modifiers.multiply(0f).getClass(), 0.66)); +// private static final WaitParseValidator DEFAULT_MAX_VALIDATOR = new WaitParseValidator(DEFAULT_MAX, false, assertWait(10000, 100000, 100000, selectors.maxReadWrite(0f).getClass(), 0.95, 0, modifiers.multiplyByAttemptsExp(0f).getClass(), 1.8)); +// private static final WaitParseValidator DEFAULT_MIN_DELTA_VALIDATOR = new WaitParseValidator(DEFAULT_SPREAD, true, assertWait(5000, MAX, 5000, selectors.maxReadWrite(0f).getClass(), 0.50, 0, modifiers.multiply(0f).getClass(), 0.5)); +// private static final RetryStrategy.ParsedStrategy DEFAULT = new RetryStrategy.ParsedStrategy(DEFAULT_WAIT_RANDOMIZER, DEFAULT_MIN, DEFAULT_MAX, DEFAULT_SPREAD, +// new RetryStrategy(DEFAULT_WAIT_RANDOMIZER, DEFAULT_MIN, DEFAULT_MAX, DEFAULT_SPREAD)); +// +// private static List VALIDATE = ImmutableList.of( +// new WaitParseValidator("p95(rw)", false, assertWait(0, MAX, MAX, selectors.maxReadWrite(0f).getClass(), 0.95, 0, modifiers.identity().getClass(), 1)), +// new WaitParseValidator("5ms<=p50(rw)*0.66", false, assertWait(5000, MAX, MAX, selectors.maxReadWrite(0f).getClass(), 0.50, 0, modifiers.multiply(0).getClass(), 0.66)), +// new WaitParseValidator("5us <= p50(r)*1.66*attempts", true, assertWait(5, MAX, 5, selectors.read(0f).getClass(), 0.50, 0, modifiers.multiplyByAttempts(0f).getClass(), 1.66)), +// new WaitParseValidator("0<=p50(w)*0.66^attempts", true, assertWait(0, MAX, 0, selectors.write(0f).getClass(), 0.50, 0, modifiers.multiplyByAttemptsExp(0f).getClass(), 0.66)), +// new WaitParseValidator("125us", true, assertWait(125, 125, 125, selectors.constant(0).getClass(), 0.0f, 125, modifiers.identity().getClass(), 1)), +// new WaitParseValidator("5us <= p95(r)*1.8^attempts <= 100us", true, assertWait(5, 100, 5, selectors.read(0f).getClass(), 0.95, 0, modifiers.multiplyByAttemptsExp(0f).getClass(), 1.8)), +// DEFAULT_MIN_VALIDATOR, DEFAULT_MAX_VALIDATOR, DEFAULT_MIN_DELTA_VALIDATOR +// ); +// +// private static List VALIDATE_RANDOMIZER = ImmutableList.of( +// new WaitRandomizerParseValidator("quantizedexponential(0.5)", QEXP, 0.5), +// new WaitRandomizerParseValidator("exponential(2.5)", EXP, 2.5), +// new WaitRandomizerParseValidator("exp(10)", EXP, 10), +// new WaitRandomizerParseValidator("uniform", UNIFORM, 0), +// DEFAULT_WAIT_RANDOMIZER_VALIDATOR +// ); +// +// static class WaitParseValidator +// { +// final String spec; +// final boolean isMin; +// final Consumer validator; +// +// WaitParseValidator(String spec, boolean isMin, Consumer validator) +// { +// this.spec = spec; +// this.isMin = isMin; +// this.validator = validator; +// } +// +// void validate(Wait Wait) +// { +// validator.accept(Wait); +// } +// } +// +// enum WaitRandomizerType +// { +// UNIFORM(Uniform.class, (p, f) -> f.uniform()), +// EXP(Exponential.class, (p, f) -> f.exponential(p)), +// QEXP(QuantizedExponential.class, (p, f) -> f.quantizedExponential(p)); +// +// final Class clazz; +// final BiFunction getter; +// +// WaitRandomizerType(Class clazz, BiFunction getter) +// { +// this.clazz = clazz; +// this.getter = getter; +// } +// } +// +// static class WaitRandomizerParseValidator +// { +// final String spec; +// final WaitRandomizerType type; +// final double power; +// +// WaitRandomizerParseValidator(String spec, WaitRandomizerType type, double power) +// { +// this.spec = spec; +// this.type = type; +// this.power = power; +// } +// +// void validate(WaitRandomizer randomizer) +// { +// Assert.assertSame(type.clazz, randomizer.getClass()); +// if (AbstractExponential.class.isAssignableFrom(type.clazz)) +// Assert.assertEquals(power, ((AbstractExponential) randomizer).power, 0.00001); +// } +// } +// +// private static class WaitRandomizerOutputValidator +// { +// static void validate(WaitRandomizerType type, long seed, int trials, int samplesPerTrial) +// { +// Random random = new Random(seed); +// WaitRandomizer randomizer = type.getter.apply(2d, new WaitRandomizerFactory() +// { +// @Override public LongBinaryOperator uniformLongSupplier() { return (min, max) -> min + random.nextInt((int) (max - min)); } +// @Override public DoubleSupplier uniformDoubleSupplier() { return random::nextDouble; } +// }); +// +// for (int i = 0 ; i < trials ; ++i) +// { +// int min = random.nextInt(1 << 20); +// int max = min + 1024 + random.nextInt(1 << 20); +// double minMean = minMean(type, min, max); +// double maxMean = maxMean(type, min, max); +// double sampleMean = sampleMean(samplesPerTrial, min, max, randomizer); +// Assert.assertTrue(minMean <= sampleMean); +// Assert.assertTrue(maxMean >= sampleMean); +// } +// } +// +// private static double minMean(WaitRandomizerType type, int min, int max) +// { +// switch (type) +// { +// case UNIFORM: return min + (max - min) * (4d/10); +// case EXP: case QEXP: return min + (max - min) * (6d/10); +// default: throw new IllegalStateException(); +// } +// } +// +// private static double maxMean(WaitRandomizerType type, int min, int max) +// { +// switch (type) +// { +// case UNIFORM: return min + (max - min) * (6d/10); +// case EXP: case QEXP: return min + (max - min) * (8d/10); +// default: throw new IllegalStateException(); +// } +// } +// +// private static double sampleMean(int samples, int min, int max, WaitRandomizer randomizer) +// { +// double sum = 0; +// int attempts = 1; +// for (int i = 0 ; i < samples ; ++i) +// { +// long wait = randomizer.wait(min, max, attempts = (attempts & 15) + 1); +// Assert.assertTrue(wait >= min); +// Assert.assertTrue(wait <= max); +// sum += wait; +// } +// double mean = sum / samples; +// Assert.assertTrue(mean >= min); +// Assert.assertTrue(mean <= max); +// return mean; +// } +// } +// +// private static Consumer assertWait( +// long min, long max, long onFailure, +// Class selectorClass, +// double selectorPercentile, +// long selectorConst, +// Class modifierClass, +// double modifierVal +// ) +// { +// return Wait -> { +// Assert.assertEquals(min, Wait.min); +// Assert.assertEquals(max, Wait.max); +// Assert.assertEquals(onFailure, Wait.onFailure); +// Assert.assertSame(selectorClass, Wait.selector.getClass()); +// if (selectorClass == selectors.constant(0).getClass()) +// { +// LatencySupplier fail = v -> { throw new UnsupportedOperationException(); }; +// Assert.assertEquals(selectorConst, Wait.selector.select(fail, fail)); +// } +// else +// { +// AtomicReference percentile = new AtomicReference<>(); +// LatencySource set = v -> { percentile.set(v); return 0; }; +// Wait.selector.select(set, set); +// Assert.assertNotNull(percentile.get()); +// Assert.assertEquals(selectorPercentile, percentile.get(), 0.00001); +// } +// Assert.assertSame(modifierClass, Wait.modifier.getClass()); +// Assert.assertEquals(1000000L * modifierVal, Wait.modifier.modify(1000000, 1), 0.00001); +// }; +// } +// +// private static void assertParseFailure(String spec) +// { +// +// try +// { +// Wait Wait = parseWait(spec, 0, 0, 0); +// Assert.fail("expected parse failure, but got " + Wait); +// } +// catch (IllegalArgumentException e) +// { +// // expected +// } +// } +// +// @Test +// public void strategyParseTest() +// { +// for (WaitParseValidator min : VALIDATE.stream().filter(v -> v.isMin).toArray(WaitParseValidator[]::new)) +// { +// for (WaitParseValidator max : VALIDATE.stream().filter(v -> !v.isMin).toArray(WaitParseValidator[]::new)) +// { +// for (WaitParseValidator minDelta : VALIDATE.stream().filter(v -> v.isMin).toArray(WaitParseValidator[]::new)) +// { +// for (WaitRandomizerParseValidator random : VALIDATE_RANDOMIZER) +// { +// { +// ParsedStrategy parsed = parseStrategy("min=" + min.spec + ",max=" + max.spec + ",delta=" + minDelta.spec + ",random=" + random.spec, DEFAULT); +// Assert.assertEquals(parsed.min, min.spec); +// min.validate(parsed.strategy.min); +// Assert.assertEquals(parsed.max, max.spec); +// max.validate(parsed.strategy.max); +// Assert.assertEquals(parsed.spread, minDelta.spec); +// minDelta.validate(parsed.strategy.spread); +// Assert.assertEquals(parsed.waitRandomizer, random.spec); +// random.validate(parsed.strategy.waitRandomizer); +// } +// ParsedStrategy parsed = parseStrategy("random=" + random.spec, DEFAULT); +// Assert.assertEquals(parsed.min, DEFAULT_MIN_VALIDATOR.spec); +// DEFAULT_MIN_VALIDATOR.validate(parsed.strategy.min); +// Assert.assertEquals(parsed.max, DEFAULT_MAX_VALIDATOR.spec); +// DEFAULT_MAX_VALIDATOR.validate(parsed.strategy.max); +// Assert.assertEquals(parsed.spread, DEFAULT_MIN_DELTA_VALIDATOR.spec); +// DEFAULT_MIN_DELTA_VALIDATOR.validate(parsed.strategy.spread); +// Assert.assertEquals(parsed.waitRandomizer, random.spec); +// random.validate(parsed.strategy.waitRandomizer); +// } +// ParsedStrategy parsed = parseStrategy("delta=" + minDelta.spec, DEFAULT); +// Assert.assertEquals(parsed.min, DEFAULT_MIN_VALIDATOR.spec); +// DEFAULT_MIN_VALIDATOR.validate(parsed.strategy.min); +// Assert.assertEquals(parsed.max, DEFAULT_MAX_VALIDATOR.spec); +// DEFAULT_MAX_VALIDATOR.validate(parsed.strategy.max); +// Assert.assertEquals(parsed.spread, minDelta.spec); +// minDelta.validate(parsed.strategy.spread); +// } +// ParsedStrategy parsed = parseStrategy("max=" + max.spec, DEFAULT); +// Assert.assertEquals(parsed.min, DEFAULT_MIN_VALIDATOR.spec); +// DEFAULT_MIN_VALIDATOR.validate(parsed.strategy.min); +// Assert.assertEquals(parsed.max, max.spec); +// max.validate(parsed.strategy.max); +// Assert.assertEquals(parsed.spread, DEFAULT_MIN_DELTA_VALIDATOR.spec); +// DEFAULT_MIN_DELTA_VALIDATOR.validate(parsed.strategy.spread); +// } +// ParsedStrategy parsed = parseStrategy("min=" + min.spec, DEFAULT); +// Assert.assertEquals(parsed.min, min.spec); +// min.validate(parsed.strategy.min); +// Assert.assertEquals(parsed.max, DEFAULT_MAX_VALIDATOR.spec); +// DEFAULT_MAX_VALIDATOR.validate(parsed.strategy.max); +// Assert.assertEquals(parsed.spread, DEFAULT_MIN_DELTA_VALIDATOR.spec); +// DEFAULT_MIN_DELTA_VALIDATOR.validate(parsed.strategy.spread); +// } +// } +// +// @Test +// public void testParseRoundTrip() +// { +// LatencySupplierFactory selectorFactory = new LatencySupplierFactory() +// { +// LatencySupplierFactory delegate = TimeoutStrategy.selectors; +// public LatencySelector constant(long latency) { return selector(delegate.constant(latency), String.format("%dms", latency)); } +// public LatencySelector read(double percentile) { return selector(delegate.read(percentile), String.format("p%d(r)", (int) (percentile * 100))); } +// public LatencySelector write(double percentile) { return selector(delegate.write(percentile), String.format("p%d(w)", (int) (percentile * 100))); } +// public LatencySelector maxReadWrite(double percentile) { return selector(delegate.maxReadWrite(percentile), String.format("p%d(rw)", (int) percentile * 100)); } +// +// private LatencySelector selector(LatencySelector selector, String str) { +// return new LatencySelector() +// { +// public long select(LatencySupplier read, LatencySupplier write) +// { +// return selector.select(read, write); +// } +// +// public String toString() +// { +// return str; +// } +// }; +// } +// }; +// +// LatencyModifierFactory modifierFactory = new LatencyModifierFactory() +// { +// LatencyModifierFactory delegate = modifiers; +// public LatencyModifier identity() { return modifier(delegate.identity(), ""); } +// public LatencyModifier multiply(double constant) { return modifier(delegate.multiply(constant), String.format(" * %.2f", constant)); } +// public LatencyModifier multiplyByAttempts(double multiply) { return modifier(delegate.multiplyByAttempts(multiply), String.format(" * %.2f * attempts", multiply)); } +// public LatencyModifier multiplyByAttemptsExp(double base) { return modifier(delegate.multiplyByAttemptsExp(base), String.format(" * %.2f ^ attempts", base)); } +// +// private LatencyModifier modifier(LatencyModifier modifier, String str) { +// return new LatencyModifier() +// { +// @Inline +// public long modify(long latency, int attempts) +// { +// return modifier.modify(latency, attempts); +// } +// +// public String toString() +// { +// return str; +// } +// }; +// } +// }; +// +// LatencyModifier[] latencyModifiers = new LatencyModifier[]{ +// modifierFactory.multiply(0.5), +// modifierFactory.multiplyByAttempts(0.5), +// modifierFactory.multiplyByAttemptsExp(0.5) +// }; +// +// LatencySelector[] latencySelectors = new LatencySelector[]{ +// selectorFactory.read(0.5), +// selectorFactory.write(0.5), +// selectorFactory.maxReadWrite(0.99) +// }; +// +// for (boolean min : new boolean[] { true, false}) +// { +// String left = min ? "10ms <= " : ""; +// for (boolean max : new boolean[] { true, false}) +// { +// String right = max ? " <= 10ms" : ""; +// +// for (LatencySelector selector : latencySelectors) +// { +// for (LatencyModifier modifier : latencyModifiers) +// { +// String mid = String.format("%s%s", selector, modifier); +// String input = left + mid + right; +// Wait Wait = parseWait(input, 0, MAX, MAX, selectorFactory, modifierFactory); +// Assert.assertTrue(String.format("Wait: %d" , Wait.min), !min || Wait.min == 10000); +// Assert.assertTrue(String.format("Wait: %d" , Wait.max), !max || Wait.max == 10000); +// Assert.assertEquals(selector.toString(), Wait.selector.toString()); +// Assert.assertEquals(modifier.toString(), Wait.modifier.toString()); +// } +// } +// } +// } +// } +// +// @Test +// public void WaitParseTest() +// { +// VALIDATE.forEach(v -> v.validate(parseWait(v.spec, 0, MAX, v.isMin ? 0 : MAX))); +// } +// +// @Test +// public void waitRandomizerParseTest() +// { +// VALIDATE_RANDOMIZER.forEach(v -> v.validate(parseWaitRandomizer(v.spec))); +// } +// +// @Test +// public void waitRandomizerSampleTest() +// { +// waitRandomizerSampleTest(2); +// } +// +// private void waitRandomizerSampleTest(int count) +// { +// while (count-- > 0) +// { +// long seed = ThreadLocalRandom.current().nextLong(); +// logger.info("Seed {}", seed); +// for (WaitRandomizerType type : WaitRandomizerType.values()) +// { +// WaitRandomizerOutputValidator.validate(type, seed, 100, 1000000); +// } +// } +// } +// +// @Test +// public void WaitParseFailureTest() +// { +// assertParseFailure("10ms <= p95(r) <= 5ms"); +// assertParseFailure("10 <= p95(r)"); +// assertParseFailure("10 <= 20 <= 30"); +// assertParseFailure("p95(r) < 5"); +// assertParseFailure("p95(x)"); +// assertParseFailure("p95()"); +// assertParseFailure("p95"); +// assertParseFailure("p50(rw)+0.66"); +// } +// +// @Test +// public void testBackoffTime() +// { +// RetryStrategy strategy = parseStrategy("min=0ms,max=100ms,random=uniform", DEFAULT).strategy; +// double total = 0; +// int count = 100000; +// for (int i = 0 ; i < count ; ++i) +// { +// long now = System.nanoTime(); +// long waitUntil = strategy.computeWaitUntil(1); +// long waitLength = Math.max(waitUntil - now, 0); +// total += waitLength; +// } +// Assert.assertTrue(Math.abs(TimeUnit.MILLISECONDS.toNanos(50) - (total / count)) < TimeUnit.MILLISECONDS.toNanos(1L)); +// } +// +// @Test +// public void testBackoffTimeElapsed() +// { +// ContentionStrategy strategy = ContentionStrategy.parseStrategy("min=0ms,max=10ms,random=uniform").strategy; +// double total = 0; +// int count = 1000; +// for (int i = 0 ; i < count ; ++i) +// { +// long start = System.nanoTime(); +// strategy.doWaitForContention(Long.MAX_VALUE, 1, null, null, null, null); +// long end = System.nanoTime(); +// total += end - start; +// } +// // make sure we have slept at least 4ms on average, given a mean wait time of 5ms +// double avg = total / count; +// double nanos = avg - TimeUnit.MILLISECONDS.toNanos(4); +// Assert.assertTrue(nanos > 0); +// } +//} diff --git a/test/unit/org/apache/cassandra/service/accord/AccordMessageSinkTest.java b/test/unit/org/apache/cassandra/service/accord/AccordMessageSinkTest.java index 7cc2d081dc..491f193fac 100644 --- a/test/unit/org/apache/cassandra/service/accord/AccordMessageSinkTest.java +++ b/test/unit/org/apache/cassandra/service/accord/AccordMessageSinkTest.java @@ -21,9 +21,13 @@ package org.apache.cassandra.service.accord; import org.junit.BeforeClass; import org.junit.Test; +import accord.api.TopologySorter; +import accord.api.TopologySorter.StaticSorter; +import accord.impl.RequestCallbacks; import accord.messages.ReadData; import accord.messages.ReadData.CommitOrReadNack; import accord.topology.TopologyUtils; +import org.apache.cassandra.service.accord.api.AccordTimeService; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -56,10 +60,10 @@ public class AccordMessageSinkTest private static final Node.Id node = new Node.Id(1); private static final AccordEndpointMapper mapping = SimpleAccordEndpointMapper.INSTANCE; private static final Topology topology = TopologyUtils.initialTopology(new Node.Id[] { node}, Ranges.of(IntKey.range(0, 100)), 1); - private static final Topologies topologies = new Topologies.Single((a, b, ignore) -> 0, topology); + private static final Topologies topologies = new Topologies.Single((TopologySorter) (StaticSorter)(a, b, ignore) -> 0, topology); private static final MessageDelivery messaging = Mockito.mock(MessageDelivery.class); - private static final AccordMessageSink sink = new AccordMessageSink(Mockito.mock(Agent.class), messaging, mapping); + private static final AccordMessageSink sink = new AccordMessageSink(Mockito.mock(Agent.class), messaging, mapping, new RequestCallbacks(new AccordTimeService())); @BeforeClass public static void setup() diff --git a/test/unit/org/apache/cassandra/service/accord/AccordTestUtils.java b/test/unit/org/apache/cassandra/service/accord/AccordTestUtils.java index c75690f2e2..a40200a6a6 100644 --- a/test/unit/org/apache/cassandra/service/accord/AccordTestUtils.java +++ b/test/unit/org/apache/cassandra/service/accord/AccordTestUtils.java @@ -50,7 +50,7 @@ import accord.local.DurableBefore; import accord.local.Node; import accord.local.Node.Id; import accord.local.NodeCommandStoreService; -import accord.local.NodeTimeService; +import accord.local.TimeService; import accord.local.PreLoadContext; import accord.local.SafeCommand; import accord.local.SafeCommandStore; @@ -368,12 +368,14 @@ public class AccordTestUtils Node.Id node = new Id(1); NodeCommandStoreService time = new NodeCommandStoreService() { - private ToLongFunction elapsed = NodeTimeService.elapsedWrapperFromNonMonotonicSource(TimeUnit.MICROSECONDS, this::now); + private ToLongFunction elapsed = TimeService.elapsedWrapperFromNonMonotonicSource(TimeUnit.MICROSECONDS, this::now); @Override public Id id() { return node;} @Override public DurableBefore durableBefore() { return DurableBefore.EMPTY; } + @Override public long epoch() {return 1; } @Override public long now() {return now.getAsLong(); } + @Override public Timestamp uniqueNow() { return uniqueNow(Timestamp.NONE); } @Override public Timestamp uniqueNow(Timestamp atLeast) { return Timestamp.fromValues(1, now.getAsLong(), node); } @Override public long elapsed(TimeUnit timeUnit) { return elapsed.applyAsLong(timeUnit); } }; @@ -390,12 +392,13 @@ public class AccordTestUtils { NodeCommandStoreService time = new NodeCommandStoreService() { - private ToLongFunction elapsed = NodeTimeService.elapsedWrapperFromNonMonotonicSource(TimeUnit.MICROSECONDS, this::now); + private ToLongFunction elapsed = TimeService.elapsedWrapperFromNonMonotonicSource(TimeUnit.MICROSECONDS, this::now); @Override public DurableBefore durableBefore() { return DurableBefore.EMPTY; } @Override public Id id() { return node;} @Override public long epoch() {return 1; } @Override public long now() {return now.getAsLong(); } + @Override public Timestamp uniqueNow() { return uniqueNow(Timestamp.NONE); } @Override public Timestamp uniqueNow(Timestamp atLeast) { return Timestamp.fromValues(1, now.getAsLong(), node); } @Override public long elapsed(TimeUnit timeUnit) { return elapsed.applyAsLong(timeUnit); } diff --git a/test/unit/org/apache/cassandra/service/accord/EpochSyncTest.java b/test/unit/org/apache/cassandra/service/accord/EpochSyncTest.java index f5faef62c9..8344c09dc6 100644 --- a/test/unit/org/apache/cassandra/service/accord/EpochSyncTest.java +++ b/test/unit/org/apache/cassandra/service/accord/EpochSyncTest.java @@ -54,7 +54,7 @@ import accord.api.Scheduler; import accord.impl.SizeOfIntersectionSorter; import accord.impl.TestAgent; import accord.local.Node; -import accord.local.NodeTimeService; +import accord.local.TimeService; import accord.primitives.Ranges; import accord.topology.Topology; import accord.topology.TopologyManager; @@ -666,7 +666,7 @@ public class EpochSyncTest this.token = token; this.epoch = epoch; // TODO (review): Should there be a real scheduler here? Is it possible to adapt the Scheduler interface to scheduler used in this test? - this.topology = new TopologyManager(SizeOfIntersectionSorter.SUPPLIER, new TestAgent.RethrowAgent(), id, Scheduler.NEVER_RUN_SCHEDULED, NodeTimeService.elapsedWrapperFromNonMonotonicSource(TimeUnit.MILLISECONDS, globalExecutor::currentTimeMillis), LocalConfig.DEFAULT); + this.topology = new TopologyManager(SizeOfIntersectionSorter.SUPPLIER, new TestAgent.RethrowAgent(), id, Scheduler.NEVER_RUN_SCHEDULED, TimeService.ofNonMonotonic(globalExecutor::currentTimeMillis, TimeUnit.MILLISECONDS), LocalConfig.DEFAULT); AccordConfigurationService.DiskStateManager instance = MockDiskStateManager.instance; config = new AccordConfigurationService(node, messagingService, failureDetector, instance, scheduler); config.registerListener(new ConfigurationService.Listener() diff --git a/test/unit/org/apache/cassandra/service/accord/SimulatedAccordCommandStore.java b/test/unit/org/apache/cassandra/service/accord/SimulatedAccordCommandStore.java index 72581b0cf2..7da1a51e13 100644 --- a/test/unit/org/apache/cassandra/service/accord/SimulatedAccordCommandStore.java +++ b/test/unit/org/apache/cassandra/service/accord/SimulatedAccordCommandStore.java @@ -41,7 +41,7 @@ import accord.local.CommandStores; import accord.local.DurableBefore; import accord.local.Node; import accord.local.NodeCommandStoreService; -import accord.local.NodeTimeService; +import accord.local.TimeService; import accord.local.PreLoadContext; import accord.local.SafeCommand; import accord.local.SafeCommandStore; @@ -118,10 +118,16 @@ public class SimulatedAccordCommandStore implements AutoCloseable this.nodeId = AccordTopology.tcmIdToAccord(ClusterMetadata.currentNullable().myNodeId()); this.storeService = new NodeCommandStoreService() { - private final ToLongFunction elapsed = NodeTimeService.elapsedWrapperFromNonMonotonicSource(TimeUnit.NANOSECONDS, this::now); + private final ToLongFunction elapsed = TimeService.elapsedWrapperFromNonMonotonicSource(TimeUnit.NANOSECONDS, this::now); @Override public DurableBefore durableBefore() { return DurableBefore.EMPTY; } + @Override + public Timestamp uniqueNow() + { + return uniqueNow(Timestamp.NONE); + } + @Override public Node.Id id() { diff --git a/test/unit/org/apache/cassandra/service/accord/repair/RequiredResponseTrackerTest.java b/test/unit/org/apache/cassandra/service/accord/repair/RequiredResponseTrackerTest.java index 022543bb3a..f2900a3595 100644 --- a/test/unit/org/apache/cassandra/service/accord/repair/RequiredResponseTrackerTest.java +++ b/test/unit/org/apache/cassandra/service/accord/repair/RequiredResponseTrackerTest.java @@ -27,6 +27,7 @@ import org.junit.BeforeClass; import org.junit.Test; import accord.api.TopologySorter; +import accord.api.TopologySorter.StaticSorter; import accord.coordinate.tracking.RequestStatus; import accord.local.Node; import accord.topology.Topologies; @@ -58,7 +59,7 @@ public class RequiredResponseTrackerTest private static final Location LOCATION = new Location("DC1", "RACK1"); private static final List> RANGES = ImmutableList.of(range(-100, 0), range(0, 100), range(100, -100)); - private static final TopologySorter TOPOLOGY_SORTER = (node1, node2, shards) -> node1.compareTo(node2); + private static final TopologySorter TOPOLOGY_SORTER = (StaticSorter)(node1, node2, shards) -> node1.compareTo(node2); @BeforeClass public static void beforeClass() throws Throwable diff --git a/test/unit/org/apache/cassandra/service/paxos/ContentionStrategyTest.java b/test/unit/org/apache/cassandra/service/paxos/ContentionStrategyTest.java deleted file mode 100644 index 8b67c425b1..0000000000 --- a/test/unit/org/apache/cassandra/service/paxos/ContentionStrategyTest.java +++ /dev/null @@ -1,466 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.service.paxos; - -import java.util.List; -import java.util.Random; -import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.DoubleSupplier; -import java.util.function.LongBinaryOperator; - -import com.google.common.collect.ImmutableList; -import org.junit.Assert; -import org.junit.Test; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import net.nicoulaj.compilecommand.annotations.Inline; -import org.apache.cassandra.config.DatabaseDescriptor; - -import static org.apache.cassandra.service.paxos.ContentionStrategy.*; -import static org.apache.cassandra.service.paxos.ContentionStrategy.WaitRandomizerFactory.*; -import static org.apache.cassandra.service.paxos.ContentionStrategyTest.WaitRandomizerType.*; - -public class ContentionStrategyTest -{ - private static final Logger logger = LoggerFactory.getLogger(ContentionStrategyTest.class); - - static - { - DatabaseDescriptor.daemonInitialization(); - } - - private static final long MAX = maxQueryTimeoutMicros()/2; - - private static final WaitParseValidator DEFAULT_WAIT_RANDOMIZER_VALIDATOR = new WaitParseValidator(defaultWaitRandomizer(), QEXP, 1.5); - private static final BoundParseValidator DEFAULT_MIN_VALIDATOR = new BoundParseValidator(defaultMinWait(), true, assertBound(0, MAX, 0, selectors.maxReadWrite(0f).getClass(), 0.50, 0, modifiers.multiply(0f).getClass(), 0.66)); - private static final BoundParseValidator DEFAULT_MAX_VALIDATOR = new BoundParseValidator(defaultMaxWait(), false, assertBound(10000, 100000, 100000, selectors.maxReadWrite(0f).getClass(), 0.95, 0, modifiers.multiplyByAttemptsExp(0f).getClass(), 1.8)); - private static final BoundParseValidator DEFAULT_MIN_DELTA_VALIDATOR = new BoundParseValidator(defaultMinDelta(), true, assertBound(5000, MAX, 5000, selectors.maxReadWrite(0f).getClass(), 0.50, 0, modifiers.multiply(0f).getClass(), 0.5)); - - private static List VALIDATE = ImmutableList.of( - new BoundParseValidator("p95(rw)", false, assertBound(0, MAX, MAX, selectors.maxReadWrite(0f).getClass(), 0.95, 0, modifiers.identity().getClass(), 1)), - new BoundParseValidator("5ms<=p50(rw)*0.66", false, assertBound(5000, MAX, MAX, selectors.maxReadWrite(0f).getClass(), 0.50, 0, modifiers.multiply(0).getClass(), 0.66)), - new BoundParseValidator("5us <= p50(r)*1.66*attempts", true, assertBound(5, MAX, 5, selectors.read(0f).getClass(), 0.50, 0, modifiers.multiplyByAttempts(0f).getClass(), 1.66)), - new BoundParseValidator("0<=p50(w)*0.66^attempts", true, assertBound(0, MAX, 0, selectors.write(0f).getClass(), 0.50, 0, modifiers.multiplyByAttemptsExp(0f).getClass(), 0.66)), - new BoundParseValidator("125us", true, assertBound(125, 125, 125, selectors.constant(0).getClass(), 0.0f, 125, modifiers.identity().getClass(), 1)), - new BoundParseValidator("5us <= p95(r)*1.8^attempts <= 100us", true, assertBound(5, 100, 5, selectors.read(0f).getClass(), 0.95, 0, modifiers.multiplyByAttemptsExp(0f).getClass(), 1.8)), - DEFAULT_MIN_VALIDATOR, DEFAULT_MAX_VALIDATOR, DEFAULT_MIN_DELTA_VALIDATOR - ); - - private static List VALIDATE_RANDOMIZER = ImmutableList.of( - new WaitParseValidator("quantizedexponential(0.5)", QEXP, 0.5), - new WaitParseValidator("exponential(2.5)", EXP, 2.5), - new WaitParseValidator("exp(10)", EXP, 10), - new WaitParseValidator("uniform", UNIFORM, 0), - DEFAULT_WAIT_RANDOMIZER_VALIDATOR - ); - - static class BoundParseValidator - { - final String spec; - final boolean isMin; - final Consumer validator; - - BoundParseValidator(String spec, boolean isMin, Consumer validator) - { - this.spec = spec; - this.isMin = isMin; - this.validator = validator; - } - - void validate(Bound bound) - { - validator.accept(bound); - } - } - - enum WaitRandomizerType - { - UNIFORM(Uniform.class, (p, f) -> f.uniform()), - EXP(Exponential.class, (p, f) -> f.exponential(p)), - QEXP(QuantizedExponential.class, (p, f) -> f.quantizedExponential(p)); - - final Class clazz; - final BiFunction getter; - - WaitRandomizerType(Class clazz, BiFunction getter) - { - this.clazz = clazz; - this.getter = getter; - } - } - - - static class WaitParseValidator - { - final String spec; - final WaitRandomizerType type; - final double power; - - WaitParseValidator(String spec, WaitRandomizerType type, double power) - { - this.spec = spec; - this.type = type; - this.power = power; - } - - void validate(WaitRandomizer randomizer) - { - Assert.assertSame(type.clazz, randomizer.getClass()); - if (AbstractExponential.class.isAssignableFrom(type.clazz)) - Assert.assertEquals(power, ((AbstractExponential) randomizer).power, 0.00001); - } - } - - private static class WaitRandomizerOutputValidator - { - static void validate(WaitRandomizerType type, long seed, int trials, int samplesPerTrial) - { - Random random = new Random(seed); - WaitRandomizer randomizer = type.getter.apply(2d, new WaitRandomizerFactory() - { - @Override public LongBinaryOperator uniformLongSupplier() { return (min, max) -> min + random.nextInt((int) (max - min)); } - @Override public DoubleSupplier uniformDoubleSupplier() { return random::nextDouble; } - }); - - for (int i = 0 ; i < trials ; ++i) - { - int min = random.nextInt(1 << 20); - int max = min + 1024 + random.nextInt(1 << 20); - double minMean = minMean(type, min, max); - double maxMean = maxMean(type, min, max); - double sampleMean = sampleMean(samplesPerTrial, min, max, randomizer); - Assert.assertTrue(minMean <= sampleMean); - Assert.assertTrue(maxMean >= sampleMean); - } - } - - private static double minMean(WaitRandomizerType type, int min, int max) - { - switch (type) - { - case UNIFORM: return min + (max - min) * (4d/10); - case EXP: case QEXP: return min + (max - min) * (6d/10); - default: throw new IllegalStateException(); - } - } - - private static double maxMean(WaitRandomizerType type, int min, int max) - { - switch (type) - { - case UNIFORM: return min + (max - min) * (6d/10); - case EXP: case QEXP: return min + (max - min) * (8d/10); - default: throw new IllegalStateException(); - } - } - - private static double sampleMean(int samples, int min, int max, WaitRandomizer randomizer) - { - double sum = 0; - int attempts = 1; - for (int i = 0 ; i < samples ; ++i) - { - long wait = randomizer.wait(min, max, attempts = (attempts & 15) + 1); - Assert.assertTrue(wait >= min); - Assert.assertTrue(wait <= max); - sum += wait; - } - double mean = sum / samples; - Assert.assertTrue(mean >= min); - Assert.assertTrue(mean <= max); - return mean; - } - } - - private static Consumer assertBound( - long min, long max, long onFailure, - Class selectorClass, - double selectorPercentile, - long selectorConst, - Class modifierClass, - double modifierVal - ) - { - return bound -> { - Assert.assertEquals(min, bound.min); - Assert.assertEquals(max, bound.max); - Assert.assertEquals(onFailure, bound.onFailure); - Assert.assertSame(selectorClass, bound.selector.getClass()); - if (selectorClass == selectors.constant(0).getClass()) - { - LatencySupplier fail = v -> { throw new UnsupportedOperationException(); }; - Assert.assertEquals(selectorConst, bound.selector.select(fail, fail)); - } - else - { - AtomicReference percentile = new AtomicReference<>(); - LatencySupplier set = v -> { percentile.set(v); return 0; }; - bound.selector.select(set, set); - Assert.assertNotNull(percentile.get()); - Assert.assertEquals(selectorPercentile, percentile.get(), 0.00001); - } - Assert.assertSame(modifierClass, bound.modifier.getClass()); - Assert.assertEquals(1000000L * modifierVal, bound.modifier.modify(1000000, 1), 0.00001); - }; - } - - private static void assertParseFailure(String spec) - { - - try - { - Bound bound = parseBound(spec, false); - Assert.fail("expected parse failure, but got " + bound); - } - catch (IllegalArgumentException e) - { - // expected - } - } - - @Test - public void strategyParseTest() - { - for (BoundParseValidator min : VALIDATE.stream().filter(v -> v.isMin).toArray(BoundParseValidator[]::new)) - { - for (BoundParseValidator max : VALIDATE.stream().filter(v -> !v.isMin).toArray(BoundParseValidator[]::new)) - { - for (BoundParseValidator minDelta : VALIDATE.stream().filter(v -> v.isMin).toArray(BoundParseValidator[]::new)) - { - for (WaitParseValidator random : VALIDATE_RANDOMIZER) - { - { - ParsedStrategy parsed = parseStrategy("min=" + min.spec + ",max=" + max.spec + ",delta=" + minDelta.spec + ",random=" + random.spec); - Assert.assertEquals(parsed.min, min.spec); - min.validate(parsed.strategy.min); - Assert.assertEquals(parsed.max, max.spec); - max.validate(parsed.strategy.max); - Assert.assertEquals(parsed.minDelta, minDelta.spec); - minDelta.validate(parsed.strategy.minDelta); - Assert.assertEquals(parsed.waitRandomizer, random.spec); - random.validate(parsed.strategy.waitRandomizer); - } - ParsedStrategy parsed = parseStrategy("random=" + random.spec); - Assert.assertEquals(parsed.min, DEFAULT_MIN_VALIDATOR.spec); - DEFAULT_MIN_VALIDATOR.validate(parsed.strategy.min); - Assert.assertEquals(parsed.max, DEFAULT_MAX_VALIDATOR.spec); - DEFAULT_MAX_VALIDATOR.validate(parsed.strategy.max); - Assert.assertEquals(parsed.minDelta, DEFAULT_MIN_DELTA_VALIDATOR.spec); - DEFAULT_MIN_DELTA_VALIDATOR.validate(parsed.strategy.minDelta); - Assert.assertEquals(parsed.waitRandomizer, random.spec); - random.validate(parsed.strategy.waitRandomizer); - } - ParsedStrategy parsed = parseStrategy("delta=" + minDelta.spec); - Assert.assertEquals(parsed.min, DEFAULT_MIN_VALIDATOR.spec); - DEFAULT_MIN_VALIDATOR.validate(parsed.strategy.min); - Assert.assertEquals(parsed.max, DEFAULT_MAX_VALIDATOR.spec); - DEFAULT_MAX_VALIDATOR.validate(parsed.strategy.max); - Assert.assertEquals(parsed.minDelta, minDelta.spec); - minDelta.validate(parsed.strategy.minDelta); - } - ParsedStrategy parsed = parseStrategy("max=" + max.spec); - Assert.assertEquals(parsed.min, DEFAULT_MIN_VALIDATOR.spec); - DEFAULT_MIN_VALIDATOR.validate(parsed.strategy.min); - Assert.assertEquals(parsed.max, max.spec); - max.validate(parsed.strategy.max); - Assert.assertEquals(parsed.minDelta, DEFAULT_MIN_DELTA_VALIDATOR.spec); - DEFAULT_MIN_DELTA_VALIDATOR.validate(parsed.strategy.minDelta); - } - ParsedStrategy parsed = parseStrategy("min=" + min.spec); - Assert.assertEquals(parsed.min, min.spec); - min.validate(parsed.strategy.min); - Assert.assertEquals(parsed.max, DEFAULT_MAX_VALIDATOR.spec); - DEFAULT_MAX_VALIDATOR.validate(parsed.strategy.max); - Assert.assertEquals(parsed.minDelta, DEFAULT_MIN_DELTA_VALIDATOR.spec); - DEFAULT_MIN_DELTA_VALIDATOR.validate(parsed.strategy.minDelta); - } - } - - @Test - public void testParseRoundTrip() - { - LatencySelectorFactory selectorFactory = new LatencySelectorFactory() - { - LatencySelectorFactory delegate = ContentionStrategy.selectors; - public LatencySelector constant(long latency) { return selector(delegate.constant(latency), String.format("%dms", latency)); } - public LatencySelector read(double percentile) { return selector(delegate.read(percentile), String.format("p%d(r)", (int) (percentile * 100))); } - public LatencySelector write(double percentile) { return selector(delegate.write(percentile), String.format("p%d(w)", (int) (percentile * 100))); } - public LatencySelector maxReadWrite(double percentile) { return selector(delegate.maxReadWrite(percentile), String.format("p%d(rw)", (int) percentile * 100)); } - - private LatencySelector selector(LatencySelector selector, String str) { - return new LatencySelector() - { - public long select(LatencySupplier read, LatencySupplier write) - { - return selector.select(read, write); - } - - public String toString() - { - return str; - } - }; - } - }; - - LatencyModifierFactory modifierFactory = new LatencyModifierFactory() - { - LatencyModifierFactory delegate = ContentionStrategy.modifiers; - public LatencyModifier identity() { return modifier(delegate.identity(), ""); } - public LatencyModifier multiply(double constant) { return modifier(delegate.multiply(constant), String.format(" * %.2f", constant)); } - public LatencyModifier multiplyByAttempts(double multiply) { return modifier(delegate.multiplyByAttempts(multiply), String.format(" * %.2f * attempts", multiply)); } - public LatencyModifier multiplyByAttemptsExp(double base) { return modifier(delegate.multiplyByAttemptsExp(base), String.format(" * %.2f ^ attempts", base)); } - - private LatencyModifier modifier(LatencyModifier modifier, String str) { - return new LatencyModifier() - { - @Inline - public long modify(long latency, int attempts) - { - return modifier.modify(latency, attempts); - } - - public String toString() - { - return str; - } - }; - } - }; - - LatencyModifier[] latencyModifiers = new LatencyModifier[]{ - modifierFactory.multiply(0.5), - modifierFactory.multiplyByAttempts(0.5), - modifierFactory.multiplyByAttemptsExp(0.5) - }; - - LatencySelector[] latencySelectors = new LatencySelector[]{ - selectorFactory.read(0.5), - selectorFactory.write(0.5), - selectorFactory.maxReadWrite(0.99) - }; - - for (boolean min : new boolean[] { true, false}) - { - String left = min ? "10ms <= " : ""; - for (boolean max : new boolean[] { true, false}) - { - String right = max ? " <= 10ms" : ""; - - for (LatencySelector selector : latencySelectors) - { - for (LatencyModifier modifier : latencyModifiers) - { - String mid = String.format("%s%s", selector, modifier); - String input = left + mid + right; - Bound bound = parseBound(input, false, selectorFactory, modifierFactory); - Assert.assertTrue(String.format("Bound: %d" , bound.min), !min || bound.min == 10000); - Assert.assertTrue(String.format("Bound: %d" , bound.max), !max || bound.max == 10000); - Assert.assertEquals(selector.toString(), bound.selector.toString()); - Assert.assertEquals(modifier.toString(), bound.modifier.toString()); - } - } - } - } - } - - @Test - public void boundParseTest() - { - VALIDATE.forEach(v -> v.validate(parseBound(v.spec, v.isMin))); - } - - @Test - public void waitRandomizerParseTest() - { - VALIDATE_RANDOMIZER.forEach(v -> v.validate(parseWaitRandomizer(v.spec))); - } - - @Test - public void waitRandomizerSampleTest() - { - waitRandomizerSampleTest(2); - } - - private void waitRandomizerSampleTest(int count) - { - while (count-- > 0) - { - long seed = ThreadLocalRandom.current().nextLong(); - logger.info("Seed {}", seed); - for (WaitRandomizerType type : WaitRandomizerType.values()) - { - WaitRandomizerOutputValidator.validate(type, seed, 100, 1000000); - } - } - } - - @Test - public void boundParseFailureTest() - { - assertParseFailure("10ms <= p95(r) <= 5ms"); - assertParseFailure("10 <= p95(r)"); - assertParseFailure("10 <= 20 <= 30"); - assertParseFailure("p95(r) < 5"); - assertParseFailure("p95(x)"); - assertParseFailure("p95()"); - assertParseFailure("p95"); - assertParseFailure("p50(rw)+0.66"); - } - - @Test - public void testBackoffTime() - { - ContentionStrategy strategy = parseStrategy("min=0ms,max=100ms,random=uniform").strategy; - double total = 0; - int count = 100000; - for (int i = 0 ; i < count ; ++i) - { - long now = System.nanoTime(); - long waitUntil = strategy.computeWaitUntilForContention(1, null, null, null, null); - long waitLength = Math.max(waitUntil - now, 0); - total += waitLength; - } - Assert.assertTrue(Math.abs(TimeUnit.MILLISECONDS.toNanos(50) - (total / count)) < TimeUnit.MILLISECONDS.toNanos(1L)); - } - - @Test - public void testBackoffTimeElapsed() - { - ContentionStrategy strategy = parseStrategy("min=0ms,max=10ms,random=uniform").strategy; - double total = 0; - int count = 1000; - for (int i = 0 ; i < count ; ++i) - { - long start = System.nanoTime(); - strategy.doWaitForContention(Long.MAX_VALUE, 1, null, null, null, null); - long end = System.nanoTime(); - total += end - start; - } - // make sure we have slept at least 4ms on average, given a mean wait time of 5ms - double avg = total / count; - double nanos = avg - TimeUnit.MILLISECONDS.toNanos(4); - Assert.assertTrue(nanos > 0); - } -}