From 82ae3adcb26fa34ca1ff64bf0cbcebb009367d5b Mon Sep 17 00:00:00 2001 From: Benedict Elliott Smith Date: Sat, 5 Oct 2024 12:32:06 +0100 Subject: [PATCH] ExclusiveSyncPoints should always wait for a simple quorum split JournalKey in journal table so we can index it reorder journal fields so we can easily index on route (when present) use Message.expiresAtNanos for callback expiration do not notify slow for range barriers Accord: Do not contact faulty replicas, and promptly report slow replies for preaccept/read. Do not wait for stale or left nodes for durability. --- modules/accord | 2 +- .../apache/cassandra/config/AccordSpec.java | 2 + .../db/compaction/CompactionIterator.java | 5 +- .../apache/cassandra/journal/Compactor.java | 2 +- .../cassandra/journal/InMemoryIndex.java | 12 +- .../org/apache/cassandra/journal/Segment.java | 1 + .../cassandra/journal/StaticSegment.java | 7 +- .../cassandra/metrics/AccordMetrics.java | 10 +- src/java/org/apache/cassandra/net/Verb.java | 98 +-- .../cassandra/service/RetryStrategy.java | 299 ++++++++++ .../cassandra/service/TimeoutStrategy.java | 368 ++++++++++++ .../service/accord/AccordCommandStore.java | 9 +- .../service/accord/AccordJournal.java | 40 +- .../service/accord/AccordJournalTable.java | 29 +- .../service/accord/AccordKeyspace.java | 69 ++- .../service/accord/AccordMessageSink.java | 60 +- .../accord/AccordResponseVerbHandler.java | 74 +++ .../accord/AccordSafeCommandStore.java | 4 +- .../accord/AccordSegmentCompactor.java | 7 +- .../service/accord/AccordService.java | 48 +- .../service/accord/AccordStateCache.java | 2 - .../service/accord/AccordVerbHandler.java | 2 +- .../accord/CommandsForRangesLoader.java | 32 +- .../service/accord/IAccordService.java | 4 +- .../cassandra/service/accord/JournalKey.java | 34 +- .../service/accord/SavedCommand.java | 26 +- .../service/accord/api/AccordTimeService.java | 47 ++ .../accord/api/AccordTopologySorter.java | 27 +- .../accord/api/CompositeTopologySorter.java | 11 + .../accord/repair/RepairSyncPointAdapter.java | 2 +- .../repair/RequiredResponseTracker.java | 4 +- .../serializers/ReadDataSerializers.java | 2 +- .../service/paxos/ContentionStrategy.java | 558 ++---------------- .../distributed/test/ForBenchmarks.java | 36 ++ .../test/accord/AccordLoadTest.java | 300 ++++++---- .../accord/AccordJournalCompactionTest.java | 1 + .../apache/cassandra/journal/IndexTest.java | 4 +- .../cassandra/service/RetryStrategyTest.java | 482 +++++++++++++++ .../service/accord/AccordMessageSinkTest.java | 8 +- .../service/accord/AccordTestUtils.java | 9 +- .../service/accord/EpochSyncTest.java | 4 +- .../accord/SimulatedAccordCommandStore.java | 10 +- .../repair/RequiredResponseTrackerTest.java | 3 +- .../service/paxos/ContentionStrategyTest.java | 466 --------------- 44 files changed, 1919 insertions(+), 1301 deletions(-) create mode 100644 src/java/org/apache/cassandra/service/RetryStrategy.java create mode 100644 src/java/org/apache/cassandra/service/TimeoutStrategy.java create mode 100644 src/java/org/apache/cassandra/service/accord/AccordResponseVerbHandler.java create mode 100644 src/java/org/apache/cassandra/service/accord/api/AccordTimeService.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/ForBenchmarks.java create mode 100644 test/unit/org/apache/cassandra/service/RetryStrategyTest.java delete mode 100644 test/unit/org/apache/cassandra/service/paxos/ContentionStrategyTest.java 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); - } -}